Netdev List
 help / color / mirror / Atom feed
* [RFC] net: towards a generic PON framework
@ 2026-09-26 17:46 Gaoyang Wei
  2026-09-26 19:28 ` Matheus Sampaio Queiroga
                   ` (2 more replies)
  0 siblings, 3 replies; 26+ messages in thread
From: Gaoyang Wei @ 2026-09-26 17:46 UTC (permalink / raw)
  To: netdev
  Cc: pbs05, Matheus Sampaio Queiroga, Benjamin Larsson,
	Lorenzo Bianconi, Andrew Lunn, Russell King

Hi all,

Several efforts now support, or are working towards supporting,
Passive Optical Network (PON) hardware on Linux and OpenWrt.  I would
like to ask for feedback on where the kernel/userspace and
generic/vendor-specific boundaries should be before any implementation
hardens its own userspace API.

This RFC is about the ONU/ONT (subscriber) side only.  It is not an
attempt to design an OLT subsystem.

Background
==========

PON devices which provide Ethernet user services commonly expose an
Ethernet datapath to Linux, but the hardware below that interface has
concepts which do not map directly to ordinary Ethernet networking:

  - ONU activation and line state;
  - PLOAM or MPCP;
  - ONU-ID and Alloc-ID;
  - T-CONTs and GEM ports for ITU-T PON;
  - LLIDs for EPON;
  - encryption/key state;
  - an OMCI or OAM management channel;
  - a burst-mode optical frontend.

Vendor BSPs commonly expose these through private ioctls, private
procfs/sysfs attributes, vendor-specific libraries, and userspace
daemons.

There are now multiple open implementations or implementation efforts,
so this seems like a useful point to discuss which concepts, if any,
should become common Linux abstractions.

Current implementations and related work
========================================

One implementation currently targets Airoha AN7581/AN7583:

  https://github.com/pbs05/openwrt-pon-drivers
  https://github.com/pbs05/openwrt-pon-userspace
  https://github.com/pbs05/ponwrt

It separates the Ethernet datapath from the management plane.  The
kernel handles the PON MAC, line activation/PLOAM, hardware GEM/T-CONT
state, and the hardware datapath, while the G.988 OMCI implementation
and service provisioning are in userspace.

Raw OMCI PDUs are exposed through a dedicated net_device without a
synthetic Ethernet header and are received and transmitted by
userspace with AF_PACKET.

pbs05, the author of that implementation, has reviewed the description
of its current architecture in this RFC.

There is also ongoing EcoNet/Airoha work for older PON hardware,
including an xPON MAC/PHY implementation and OMCI work in the OpenWrt
Airoha target:

  https://github.com/openwrt/openwrt/pull/20104

Benjamin Larsson previously raised the closely related question of the
API between an xPON MAC and a fixed laser driver/BOSA or an SFP module
[1].

These efforts do not necessarily make the same architectural choices.
That is one reason I would like to discuss the common boundary before
turning any one implementation into a generic kernel API.

Goals and non-goals
===================

My current preference is for any generic PON layer to be deliberately
small and to reuse existing Linux subsystems wherever possible.

In particular, this RFC is not proposing to:

  - implement the G.988 managed-entity model in the kernel;
  - standardize timing-critical XGTC, FEC, burst timing, or DBA logic;
  - replace bridge, VLAN, TC, ethtool, hwmon, thermal, PHY, or SFP
    infrastructure with PON-specific equivalents;
  - require different PON MACs to implement activation in the same way;
  - define an OLT-side architecture.

The question is instead whether Linux needs a small common model for
PON-specific line state and bearer resources, and how that model should
interact with existing networking and optical subsystems.

Possible layering
=================

A possible split looks like this:

                         userspace

                 +----------------------+
                 | OMCI / OAM software  |
                 +----------+-----------+
                            |
                     management PDUs
                            |
  ================================================================
                            |
                          kernel
                            |
                 +----------+----------+
                 |                     |
           PON-specific            Ethernet
           control plane           datapath
                 |                     |
          vendor PON driver        net_device
                 |                     |
           MAC / PCS / PMA     bridge / VLAN / TC
                 |
          optical frontend
             /         \
           SFP       fixed BOSA/laser driver

Where the user-facing service is Ethernet, its datapath should remain
an ordinary net_device.  Linux should not need to understand XGTC
framing, burst timing, or FEC in order to forward Ethernet packets.

Hardware which implements those functions should continue to do so,
with the vendor driver programming the corresponding hardware state.

A PON-specific kernel interface, if one is needed, would then expose
only concepts which cannot reasonably be represented by an existing
Linux subsystem.

Potential generic concepts
==========================

For ITU-T PON, possible common concepts include:

  - PON protocol/mode;
  - line/activation state;
  - ONU identity;
  - Alloc-ID / T-CONT resources;
  - GEM ports;
  - management-channel association;
  - security/key-slot state where software must program the MAC.

For EPON, the equivalent model would include LLIDs and MPCP state.

I do not think all protocol state necessarily needs to become generic
kernel code.  For example, one PON MAC may require software to
participate in PLOAM processing while another may implement most of
the same state machine in hardware or firmware.

A generic layer could therefore expose common state and resources
without requiring every driver to implement activation in the same
way.

OMCI and management packet transport
====================================

For ITU-T PON, I would prefer to keep the G.988 managed-entity model,
MIB handling, and service provisioning logic in userspace.

OMCI is a packet-oriented management protocol.  The AN7581/AN7583
implementation mentioned above exposes raw OMCI PDUs through a
dedicated net_device, without an artificial Ethernet header, and uses
AF_PACKET in userspace.

This has some useful properties:

  - the kernel does not need to understand G.988 managed entities;
  - operator/vendor-specific MEs do not become part of a kernel ABI;
  - packet capture and protocol analysis are straightforward;
  - retransmission, MIB handling, and service graph resolution remain
    outside the hardware driver.

I am not claiming that a net_device is necessarily the correct
upstream ABI.  I would particularly appreciate feedback on whether a
packet-oriented network interface is appropriate, or whether another
existing interface, Generic Netlink, or a character device would be a
better fit.

EPON OAM is a different protocol and need not share OMCI semantics or
userspace code.  The question is only whether management protocols of
this kind should use a common packet-oriented transport model where
the hardware requires host processing.

Reuse of existing subsystems
============================

I would like to avoid putting functionality into a PON layer when
Linux already has an appropriate abstraction.

For example:

  Ethernet service traffic  -> net_device
  VLAN/bridging             -> bridge / 8021q
  packet classification     -> TC where possible
  Ethernet statistics       -> ethtool
  pluggable optics          -> SFP infrastructure
  fixed-front-end telemetry -> hwmon
  thermal policy            -> thermal subsystem, where applicable
  factory calibration       -> NVMEM where appropriate

In particular, temperature, supply voltage, laser bias, and RX/TX
optical power from a fixed optical frontend should not require
PON-specific ioctls merely because the sensor is part of an ONU.

A fixed BOSA/laser driver still needs a way to communicate
link-relevant events such as LOS, TX enable, and burst-enable state to
the PON MAC.  How this should interact with the existing PHY, phylink,
and SFP infrastructure is one of the questions I would like to
discuss.

Datapath provisioning
=====================

Another open question is where service classification ends and PON
bearer configuration begins.

An OMCI userspace implementation may resolve managed entities into a
service relation similar to:

        VLAN / priority
               |
               v
            GEM port
               |
               v
            T-CONT

The GEM port and T-CONT are PON-specific bearer resources, and many
implementations represent them in hardware.

The VLAN and packet classification are not PON-specific.

It therefore seems undesirable to create a new generic PON packet
classifier if TC, bridge, and VLAN offload can express that part of
the configuration.

What remains unclear is the cleanest way for an existing Linux
classification rule to select or reference a PON bearer, especially
on hardware where that mapping is offloaded into the same DMA/QDMA
engine as the Ethernet datapath.

Questions
=========

I would particularly appreciate feedback on the following points:

1. Does Linux need a struct pon_device or equivalent at all, or can
   the required concepts be represented by existing networking
   objects?

2. If there is a generic PON object model, how much should it expose?
   Are T-CONT, GEM, and LLID appropriate generic objects, or are they
   too close to individual protocols or hardware implementations?

3. Should OMCI management PDUs be represented by a packet-oriented
   interface?  If so, is a dedicated net_device appropriate?

4. Should PLOAM/MPCP activation remain driver-specific, with only
   common line state exposed by a generic layer?

5. What should the interface between a PON MAC and fixed optical
   frontends look like, and how much should be shared with SFP,
   phylink, PHY, hwmon, and thermal infrastructure?

6. How should classification into GEM/LLID bearers interact with TC
   and existing hardware-offload APIs?

7. How much should the ITU-T PON family and IEEE EPON family share in
   one object model?  It would be useful to avoid both
   protocol-specific private APIs and an abstraction so generic that
   it no longer represents the hardware usefully.

8. Which configuration belongs in a networking control API
   (rtnetlink, Generic Netlink, etc.), and which state should only be
   observable through existing kernel subsystems?

Next steps
==========

I am intentionally not proposing a UAPI or struct definitions in this
mail.

There is working code which can be used as a reference implementation,
but I would prefer to get agreement on the subsystem boundary before
turning one vendor implementation into an API which other PON drivers
would later have to follow.

If the overall direction makes sense, the next step could be a small
RFC patch series containing only the minimum generic model together
with one real hardware consumer, rather than attempting to upstream a
complete PON stack at once.

I would also be very interested in hearing from developers working on
non-Airoha PON hardware.  A second independent hardware family would
be particularly useful for determining whether any proposed
abstraction is genuinely generic rather than an Airoha interface with
generic names.

References
==========

[1] Benjamin Larsson, "[RFC] Question regarding api between xpon mac
    and laser driver/BOSA"

    https://lore.kernel.org/r/1c774b8f-3e4d-447c-9a93-be554811cbf0@genexis.eu/

Thanks,
Gaoyang Wei

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-26 17:46 [RFC] net: towards a generic PON framework Gaoyang Wei
@ 2026-09-26 19:28 ` Matheus Sampaio Queiroga
  2026-09-26 20:21   ` Gaoyang Wei
  2026-09-27 16:57 ` Andrew Lunn
  2026-09-27 20:47 ` Benjamin Larsson
  2 siblings, 1 reply; 26+ messages in thread
From: Matheus Sampaio Queiroga @ 2026-09-26 19:28 UTC (permalink / raw)
  To: netdev, Gaoyang Wei
  Cc: pbs05, Benjamin Larsson, Lorenzo Bianconi, Andrew Lunn,
	Russell King

> Hi all,
> 
> Several efforts now support, or are working towards supporting,
> Passive Optical Network (PON) hardware on Linux and OpenWrt.  I would
> like to ask for feedback on where the kernel/userspace and
> generic/vendor-specific boundaries should be before any implementation
> hardens its own userspace API.
> 
> This RFC is about the ONU/ONT (subscriber) side only.  It is not an
> attempt to design an OLT subsystem.
> 
> Background
> ==========
> 
> PON devices which provide Ethernet user services commonly expose an
> Ethernet datapath to Linux, but the hardware below that interface has
> concepts which do not map directly to ordinary Ethernet networking:
> 
>   - ONU activation and line state;
>   - PLOAM or MPCP;
>   - ONU-ID and Alloc-ID;
>   - T-CONTs and GEM ports for ITU-T PON;
>   - LLIDs for EPON;
>   - encryption/key state;
>   - an OMCI or OAM management channel;
>   - a burst-mode optical frontend.
> 
> Vendor BSPs commonly expose these through private ioctls, private
> procfs/sysfs attributes, vendor-specific libraries, and userspace
> daemons.
> 
> There are now multiple open implementations or implementation efforts,
> so this seems like a useful point to discuss which concepts, if any,
> should become common Linux abstractions.
> 
> Current implementations and related work
> ========================================
> 
> One implementation currently targets Airoha AN7581/AN7583:
> 
>   https://github.com/pbs05/openwrt-pon-drivers
>   https://github.com/pbs05/openwrt-pon-userspace
>   https://github.com/pbs05/ponwrt
> 
> It separates the Ethernet datapath from the management plane.  The
> kernel handles the PON MAC, line activation/PLOAM, hardware GEM/T-CONT
> state, and the hardware datapath, while the G.988 OMCI implementation
> and service provisioning are in userspace.
> 
> Raw OMCI PDUs are exposed through a dedicated net_device without a
> synthetic Ethernet header and are received and transmitted by
> userspace with AF_PACKET.
> 
> pbs05, the author of that implementation, has reviewed the description
> of its current architecture in this RFC.
> 
> There is also ongoing EcoNet/Airoha work for older PON hardware,
> including an xPON MAC/PHY implementation and OMCI work in the OpenWrt
> Airoha target:
> 
>   https://github.com/openwrt/openwrt/pull/20104
> 
> Benjamin Larsson previously raised the closely related question of the
> API between an xPON MAC and a fixed laser driver/BOSA or an SFP module
> [1].
> 
> These efforts do not necessarily make the same architectural choices.
> That is one reason I would like to discuss the common boundary before
> turning any one implementation into a generic kernel API.
> 
> Goals and non-goals
> ===================
> 
> My current preference is for any generic PON layer to be deliberately
> small and to reuse existing Linux subsystems wherever possible.
> 
> In particular, this RFC is not proposing to:
> 
>   - implement the G.988 managed-entity model in the kernel;
>   - standardize timing-critical XGTC, FEC, burst timing, or DBA logic;
>   - replace bridge, VLAN, TC, ethtool, hwmon, thermal, PHY, or SFP
>     infrastructure with PON-specific equivalents;
>   - require different PON MACs to implement activation in the same way;
>   - define an OLT-side architecture.
> 
> The question is instead whether Linux needs a small common model for
> PON-specific line state and bearer resources, and how that model should
> interact with existing networking and optical subsystems.
> 
> Possible layering
> =================
> 
> A possible split looks like this:
> 
>                          userspace
> 
>                  +----------------------+
>                  | OMCI / OAM software  |
>                  +----------+-----------+
>                             |
>                      management PDUs
>                             |
>   ================================================================
>                             |
>                           kernel
>                             |
>                  +----------+----------+
>                  |                     |
>            PON-specific            Ethernet
>            control plane           datapath
>                  |                     |
>           vendor PON driver        net_device
>                  |                     |
>            MAC / PCS / PMA     bridge / VLAN / TC
>                  |
>           optical frontend
>              /         \
>            SFP       fixed BOSA/laser driver
> 
> Where the user-facing service is Ethernet, its datapath should remain
> an ordinary net_device.  Linux should not need to understand XGTC
> framing, burst timing, or FEC in order to forward Ethernet packets.
> 
> Hardware which implements those functions should continue to do so,
> with the vendor driver programming the corresponding hardware state.
> 
> A PON-specific kernel interface, if one is needed, would then expose
> only concepts which cannot reasonably be represented by an existing
> Linux subsystem.
> 
> Potential generic concepts
> ==========================
> 
> For ITU-T PON, possible common concepts include:
> 
>   - PON protocol/mode;
>   - line/activation state;
>   - ONU identity;
>   - Alloc-ID / T-CONT resources;
>   - GEM ports;
>   - management-channel association;
>   - security/key-slot state where software must program the MAC.
> 
> For EPON, the equivalent model would include LLIDs and MPCP state.
> 
> I do not think all protocol state necessarily needs to become generic
> kernel code.  For example, one PON MAC may require software to
> participate in PLOAM processing while another may implement most of
> the same state machine in hardware or firmware.
> 
> A generic layer could therefore expose common state and resources
> without requiring every driver to implement activation in the same
> way.
> 
> OMCI and management packet transport
> ====================================
> 
> For ITU-T PON, I would prefer to keep the G.988 managed-entity model,
> MIB handling, and service provisioning logic in userspace.
> 
> OMCI is a packet-oriented management protocol.  The AN7581/AN7583
> implementation mentioned above exposes raw OMCI PDUs through a
> dedicated net_device, without an artificial Ethernet header, and uses
> AF_PACKET in userspace.
> 
> This has some useful properties:
> 
>   - the kernel does not need to understand G.988 managed entities;
>   - operator/vendor-specific MEs do not become part of a kernel ABI;
>   - packet capture and protocol analysis are straightforward;
>   - retransmission, MIB handling, and service graph resolution remain
>     outside the hardware driver.
> 
> I am not claiming that a net_device is necessarily the correct
> upstream ABI.  I would particularly appreciate feedback on whether a
> packet-oriented network interface is appropriate, or whether another
> existing interface, Generic Netlink, or a character device would be a
> better fit.
> 
> EPON OAM is a different protocol and need not share OMCI semantics or
> userspace code.  The question is only whether management protocols of
> this kind should use a common packet-oriented transport model where
> the hardware requires host processing.
> 
> Reuse of existing subsystems
> ============================
> 
> I would like to avoid putting functionality into a PON layer when
> Linux already has an appropriate abstraction.
> 
> For example:
> 
>   Ethernet service traffic  -> net_device
>   VLAN/bridging             -> bridge / 8021q
>   packet classification     -> TC where possible
>   Ethernet statistics       -> ethtool
>   pluggable optics          -> SFP infrastructure
>   fixed-front-end telemetry -> hwmon
>   thermal policy            -> thermal subsystem, where applicable
>   factory calibration       -> NVMEM where appropriate
> 
> In particular, temperature, supply voltage, laser bias, and RX/TX
> optical power from a fixed optical frontend should not require
> PON-specific ioctls merely because the sensor is part of an ONU.
> 
> A fixed BOSA/laser driver still needs a way to communicate
> link-relevant events such as LOS, TX enable, and burst-enable state to
> the PON MAC.  How this should interact with the existing PHY, phylink,
> and SFP infrastructure is one of the questions I would like to
> discuss.
> 
> Datapath provisioning
> =====================
> 
> Another open question is where service classification ends and PON
> bearer configuration begins.
> 
> An OMCI userspace implementation may resolve managed entities into a
> service relation similar to:
> 
>         VLAN / priority
>                |
>                v
>             GEM port
>                |
>                v
>             T-CONT
> 
> The GEM port and T-CONT are PON-specific bearer resources, and many
> implementations represent them in hardware.
> 
> The VLAN and packet classification are not PON-specific.
> 
> It therefore seems undesirable to create a new generic PON packet
> classifier if TC, bridge, and VLAN offload can express that part of
> the configuration.
> 
> What remains unclear is the cleanest way for an existing Linux
> classification rule to select or reference a PON bearer, especially
> on hardware where that mapping is offloaded into the same DMA/QDMA
> engine as the Ethernet datapath.
> 
> Questions
> =========
> 
> I would particularly appreciate feedback on the following points:
> 
> 1. Does Linux need a struct pon_device or equivalent at all, or can
>    the required concepts be represented by existing networking
>    objects?
> 
> 2. If there is a generic PON object model, how much should it expose?
>    Are T-CONT, GEM, and LLID appropriate generic objects, or are they
>    too close to individual protocols or hardware implementations?
> 
> 3. Should OMCI management PDUs be represented by a packet-oriented
>    interface?  If so, is a dedicated net_device appropriate?
> 
> 4. Should PLOAM/MPCP activation remain driver-specific, with only
>    common line state exposed by a generic layer?
> 
> 5. What should the interface between a PON MAC and fixed optical
>    frontends look like, and how much should be shared with SFP,
>    phylink, PHY, hwmon, and thermal infrastructure?
> 
> 6. How should classification into GEM/LLID bearers interact with TC
>    and existing hardware-offload APIs?
> 
> 7. How much should the ITU-T PON family and IEEE EPON family share in
>    one object model?  It would be useful to avoid both
>    protocol-specific private APIs and an abstraction so generic that
>    it no longer represents the hardware usefully.
> 
> 8. Which configuration belongs in a networking control API
>    (rtnetlink, Generic Netlink, etc.), and which state should only be
>    observable through existing kernel subsystems?
> 
> Next steps
> ==========
> 
> I am intentionally not proposing a UAPI or struct definitions in this
> mail.
> 
> There is working code which can be used as a reference implementation,
> but I would prefer to get agreement on the subsystem boundary before
> turning one vendor implementation into an API which other PON drivers
> would later have to follow.
> 
> If the overall direction makes sense, the next step could be a small
> RFC patch series containing only the minimum generic model together
> with one real hardware consumer, rather than attempting to upstream a
> complete PON stack at once.
> 
> I would also be very interested in hearing from developers working on
> non-Airoha PON hardware.  A second independent hardware family would
> be particularly useful for determining whether any proposed
> abstraction is genuinely generic rather than an Airoha interface with
> generic names.
> 
> References
> ==========
> 
> [1] Benjamin Larsson, "[RFC] Question regarding api between xpon mac
>     and laser driver/BOSA"
> 
>     https://lore.kernel.org/r/1c774b8f-3e4d-447c-9a93-be554811cbf0@genexis.eu/
> 
> Thanks,
> Gaoyang Wei
>

Hello everyone, in my implementation part I have a system controlled by the kernel, without userspace for OMCI, everything being handled and controlled by the kernel, and having a Netlink Generice and some links in sysfs for communication with userspace for OMCI configurations with an abstraction based on what we have defined in the ITU.

I'm creating two new subsystems to control LDDLA (optical) and xPON, with them we have a way of abstracting and maintaining a standard among other platforms, not just Airoha, but all others. my OMCI follows the standard we have in the ITU and for configuration I keep a large part of the subsystem independent. Until now defining GPON and

In total, I have tests on the Airoha en7523, en751221 and en7528, tests with the en7580 family are coming as soon as I managed to test on an SBC in which I can upload the kernel.

this is my implementation:

                         userspace
                    sysfs / Generic Netlink
                              |
                              v
                    +---------------------+
                    |      net/xpon       |
                    |  struct xpon_device |
                    |---------------------|
                    | mode: GPON / EPON   |
                    | registration state  |
                    | carrier             |
                    | signal / LOS        |
                    | notifier / LEDs     |
                    +----+-----------+----+
                         |           |
              GPON       |           |       EPON
                         |           |
          +--------------+           +--------------+
          |                                         |
          v                                         v
 +-------------------+                     +-------------------+
 |   GPON PLOAM FSM  |                     |   EPON MPCP FSM   |
 | airoha_ploam.c    |                     | airoha_xpon.c     |
 |-------------------|                     |-------------------|
 | O1 -> O2 -> O3    |                     | discovery GATE    |
 |    -> O4 -> O5    |                     | REGISTER_REQUEST  |
 | ONU-ID / ranging  |                     | REGISTER / ACK    |
 | Alloc-ID / OMCC   |                     | LLID[0..7]        |
 +----+---------+----+                     +---------+---------+
      |         |                                    |
      |         | set OMCC/GEM                       |
      |         v                                    v
      |  +-----------------------+       +-----------------------+
      |  |     net/xpon/omci     |       |     net/xpon/oam      |
      |  |  struct omci_device   |       |   struct xpon_oam     |
      |  |-----------------------|       |-----------------------|
      |  | in-kernel OMCI agent  |       | per-LLID state        |
      |  | G.988 MIB / MEs       |       | 802.3ah OAMPDU        |
      |  | OLT profiles/quirks   |       | Information TLV       |
      |  | T-CONT / GEM / UNI    |       | remote OAM info       |
      |  | VLAN service graph    |       +-----------+-----------+
      |  +-----------+-----------+                   |
      |              | omci_device_ops               |
      |              v                               |
      |  +-----------------------+                   |
      |  | airoha_gpon_omci.c    |                   |
      |  | transport/backend     |                   |
      |  +-----------+-----------+                   |
      |              |                               |
      +--------------+---------------+---------------+
                                     |
                                     v
                       +---------------------------+
                       |     Airoha xPON MAC       |
                       |      airoha_xpon.c        |
                       |---------------------------|
                       | GPON MAC / PLOAM FIFO     |
                       | EPON MAC / MPCP / LLIDs   |
                       | GEM / T-CONT programming  |
                       +-------------+-------------+
                                     |
                    +----------------+----------------+
                    |                                 |
                    v                                 v
          +----------------------+       +-------------------------+
          | digital xPON PHY     |       | optical_frontend        |
          | phy-airoha-xpon.c    |       | BOSA / laser frontend   |
          |----------------------|       |-------------------------|
          | GPON / EPON mode     |       | protocol / wavelength   |
          | SERDES / CDR         |       | TX enable / rearm       |
          | burst timing         |       | TX calibration          |
          | preamble / EQD       |       | optical telemetry       |
          | FEC / signal / LOS   |       | temp / bias / TX/RX pwr |
          +----------+-----------+       +------------+------------+
                     \                            /
                      \                          /
                       +-----------+------------+
                                   |
                                   |
                                  OLT

in Airoha, GEMs and PVID/VLAN are handled by the PSE/FE/QDMA itself

in en7523.dtsi have this nodes in dts:

xpon_phy: phy@1faf0000 {
	compatible = "airoha,en7523-xpon-phy";
	reg = <0x1faf0000 0x5000>;

	pinctrl-names = "default";
	pinctrl-0 = <&pon_pinctrl>;

	resets = <&scuclk EN7523_XPON_PHY_RST>;
	reset-names = "phy";

	airoha,scu = <&scuclk>;

	tx-disable-gpios = <&pinctrl 16 GPIO_ACTIVE_HIGH>;

	#phy-cells = <0>;

	status = "disabled";
};

xpon: xmac@1fb60000 {
	compatible = "airoha,en7523-xpon";

	reg = <0x1fb60000 0x10000>;
	reg-names = "mac";

	interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>,
	             <GIC_SPI 34 IRQ_TYPE_LEVEL_HIGH>;
	interrupt-names = "mac", "dying-gasp";

	resets = <&scuclk EN7523_XPON_MAC_RST>;
	reset-names = "mac";

	phys = <&xpon_phy>;
	phy-names = "xpon";

	ethernet = <&gdm2>;
	airoha,scu = <&scuclk>;

	status = "disabled";
};

eth: ethernet@1fb50000 {
  ...
	gdm2: ethernet@2 {
		compatible = "airoha,eth-mac";
		reg = <2>;
		pcs-handle = <&pon_pcs>;

		status = "disabled";
	};
  ...
};

in devices dts:

&i2c0 {
	status = "okay";

	en7571: lddla@70 {
		compatible = "airoha,en7571";
		reg = <0x70>;

		nvmem-cells = <&en7571_bob>;
		nvmem-cell-names = "calibration";
		
		#optical-frontend-cells = <0>;
	};
};

&xpon_phy {
	status = "okay";
};

&xpon {
	status = "okay";

	airoha,gpon-serial-from-mac;
	omci-vendor-id = "TPLG";

	optical-frontends = <&en7571>;
	optical-frontend-names = "pon";

	leds = <&xpon_status>, <&los>;
	led-names = "pon", "los";
};

&gdm2 {
	status = "okay";
	phy-mode = "internal";

	nvmem-cells = <&macaddr 0>;
	nvmem-cell-names = "mac-address";

	/delete-property/ pcs-handle;
};



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-26 19:28 ` Matheus Sampaio Queiroga
@ 2026-09-26 20:21   ` Gaoyang Wei
  2026-09-26 21:55     ` Matheus Sampaio Queiroga
  0 siblings, 1 reply; 26+ messages in thread
From: Gaoyang Wei @ 2026-09-26 20:21 UTC (permalink / raw)
  To: Matheus Sampaio Queiroga, netdev
  Cc: pbs05, Benjamin Larsson, Lorenzo Bianconi, Andrew Lunn,
	Russell King

Hi Matheus,

Thanks, this is exactly the kind of second implementation I was hoping
to compare against.

 > in my implementation part I have a system controlled by the kernel,
 > without userspace for OMCI, everything being handled and controlled
 > by the kernel

This is particularly interesting because it gives us two working
design points with a very different kernel/userspace split.

The AN7581/AN7583 implementation keeps the G.988 MIB and service graph
in userspace, while your implementation places the OMCI agent, MIB,
ME handling and service graph in net/xpon/omci.

I think this is probably one of the most important questions for this
RFC.

Could you describe what led you to keep the G.988 model in the kernel?

In particular, I would be interested in whether there are operations
which require sufficiently tight coupling with the PLOAM/GEM state
that a userspace OMCI agent becomes impractical, or whether the main
reason is architectural simplicity and having one kernel-controlled
state machine.

I am also wondering whether the transport and the OMCI implementation
could be treated as two separate questions.

For example, could a generic xPON layer provide the OMCC transport and
GEM/T-CONT operations while allowing either:

   1. an in-kernel OMCI implementation, or
   2. a userspace OMCI implementation through a packet interface?

That might let us avoid deciding too early that every driver must use
the same OMCI architecture.

 > I'm creating two new subsystems to control LDDLA (optical) and xPON

The optical_frontend abstraction is also very relevant to Benjamin's
earlier RFC.

I think it would be useful to compare exactly which operations your
optical_frontend API needs against the existing PHY/SFP/hwmon
interfaces.

For example, I would expect:

   - temperature / voltage / bias / optical power -> hwmon
   - calibration storage -> NVMEM where applicable
   - LOS / TX enable / burst control -> frontend <-> PON MAC API

but I am less sure about protocol/wavelength configuration and where
that should live.

 > In total, I have tests on the Airoha en7523, en751221 and en7528

This is especially useful.

Having EN751221/EN7523/EN7528 in addition to AN7581/AN7583 gives us
more than one hardware generation to test the abstraction against.
I would very much like to identify which fields and operations are
actually common between them before defining a generic API.

Your DT example also raises another question.

The hardware relationships such as:

         phys = <&xpon_phy>;
         ethernet = <&gdm2>;
         optical-frontends = <&en7571>;

seem like natural DT topology.

On the other hand, properties such as:

         omci-vendor-id = "TPLG";
         airoha,gpon-serial-from-mac;

look more like ONU identity/provisioning policy than hardware
description to me.

I suspect those may be better supplied through NVMEM or userspace
configuration rather than becoming DT ABI, but this is another point
where upstream feedback would be useful.

Finally, your note that GEM/PVID/VLAN handling is performed by the
PSE/FE/QDMA is useful confirmation that the Ethernet classification
path and the PON bearer objects should probably not be conflated in
the generic model.

Thanks for posting the architecture and the DT examples.  This is
exactly the comparison I hoped this RFC would trigger.

Thanks,
Gaoyang Wei

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-26 20:21   ` Gaoyang Wei
@ 2026-09-26 21:55     ` Matheus Sampaio Queiroga
  2026-09-26 22:15       ` Gaoyang Wei
  0 siblings, 1 reply; 26+ messages in thread
From: Matheus Sampaio Queiroga @ 2026-09-26 21:55 UTC (permalink / raw)
  To: Gaoyang Wei
  Cc: netdev, pbs05, Benjamin Larsson, Lorenzo Bianconi, Andrew Lunn,
	Russell King

>  > in my implementation part I have a system controlled by the kernel,
>  > without userspace for OMCI, everything being handled and controlled
>  > by the kernel
>
> This is particularly interesting because it gives us two working
> design points with a very different kernel/userspace split.
>
> The AN7581/AN7583 implementation keeps the G.988 MIB and service graph
> in userspace, while your implementation places the OMCI agent, MIB,
> ME handling and service graph in net/xpon/omci.
>
> I think this is probably one of the most important questions for this
> RFC.
>
> Could you describe what led you to keep the G.988 model in the kernel?
>
> In particular, I would be interested in whether there are operations
> which require sufficiently tight coupling with the PLOAM/GEM state
> that a userspace OMCI agent becomes impractical, or whether the main
> reason is architectural simplicity and having one kernel-controlled
> state machine.
>
> I am also wondering whether the transport and the OMCI implementation
> could be treated as two separate questions.
>
> For example, could a generic xPON layer provide the OMCC transport and
> GEM/T-CONT operations while allowing either:
>
>    1. an in-kernel OMCI implementation, or
>    2. a userspace OMCI implementation through a packet interface?
>
> That might let us avoid deciding too early that every driver must use
> the same OMCI architecture.

with tests on en7523 and en751221 with some users, I saw them
attaching the OMCI network interface within a bridge, and in order not
to expose this interface again to a user, I implemented my OMCI within
the kernel, without an export network interface in the system, with
this in the driver I capture the packets and forward the data to the
OMCI subsystem in which it processes, and sends the response back to
OLT, with this I also had less overhead in the ethernet system, and
faster responses to OLT due to the new implementation.

In some internal tests, removing the net device and pasting another
method, I saw a poorly done implementation and having a lot of
overhead in CPU usage, making the system very unstable and slow for
some responses to OMCI, in addition to keeping some subsystems already
in the kernel easier to integrate if userspace is needed to make
several calls to other kernel subsystems, and today we already have
functions exposing this data within the kernel.

I am also aware of some Realtek SoCs having slightly different OMCI in
which my subsystems need to be adjusted to see this data, something
that fails in my current implementation

>  > I'm creating two new subsystems to control LDDLA (optical) and xPON
>
> The optical_frontend abstraction is also very relevant to Benjamin's
> earlier RFC.
>
> I think it would be useful to compare exactly which operations your
> optical_frontend API needs against the existing PHY/SFP/hwmon
> interfaces.
>
> For example, I would expect:
>
>    - temperature / voltage / bias / optical power -> hwmon
>    - calibration storage -> NVMEM where applicable
>    - LOS / TX enable / burst control -> frontend <-> PON MAC API
>
> but I am less sure about protocol/wavelength configuration and where
> that should live.

Airoha LDDLA are the most different we have, they do not expose a
standard SFP implementation, and emulating this data is very expensive
for what we have today, but Semtech LDDLA (which my devices use) has
the support for exposing data in the SFP standard, but not in use in
my implementation

The optical subsystem in which I implemented it already uses several
subsystems already present in Linux, so temperatures, bias and optical
power are already used within hwmon, for calibration I already use
nvmem to obtain the data and start LDDLA.
Device control is exposed by new generic functions in which to take
the data from these optical devices and use these functions to control
LOS, TX enable, burst control, etc...

There is still a lot that needs to be fixed in this part too

>  > In total, I have tests on the Airoha en7523, en751221 and en7528
>
> This is especially useful.
>
> Having EN751221/EN7523/EN7528 in addition to AN7581/AN7583 gives us
> more than one hardware generation to test the abstraction against.
> I would very much like to identify which fields and operations are
> actually common between them before defining a generic API.
>
> Your DT example also raises another question.
>
> The hardware relationships such as:
>
>          phys = <&xpon_phy>;
>          ethernet = <&gdm2>;
>          optical-frontends = <&en7571>;
>
> seem like natural DT topology.
>
> On the other hand, properties such as:
>
>          omci-vendor-id = "TPLG";
>          airoha,gpon-serial-from-mac;
>
> look more like ONU identity/provisioning policy than hardware
> description to me.
>
> I suspect those may be better supplied through NVMEM or userspace
> configuration rather than becoming DT ABI, but this is another point
> where upstream feedback would be useful.

Not all data on all devices is saved somewhere in the memory region,
so some are abstracted from several values, such as the GPON SN which
can be abstracted from the interface's mac address plus the gpon id,
these values are not yet fully ready, there are many things that need
to be fixed regarding fixed data.

a good part of the subsystem depends on values coming from the
userspace, that's why we wrote a generic netlink and sysfs for
userspace communication

This is everything I implemented:

diff --git a/drivers/net/optical/Kconfig b/drivers/net/optical/Kconfig
index 3a460ea5aa66..4c7703f2e3b6 100644
--- a/drivers/net/optical/Kconfig
+++ b/drivers/net/optical/Kconfig
@@ -25,19 +25,9 @@ config OPTICAL_FRONTEND_HWMON
   Alarm state and provider-supplied low/high thresholds are exported when
   available. Say Y when standard hwmon userspace should monitor optics.

-config OPTICAL_FRONTEND_SFP_COMPAT
- bool "Virtual SFF-8472 compatibility bus"
- depends on I2C
- default y
- help
-  Allow a soldered optical frontend to expose a virtual SFP I2C bus with
-  SFF-8472 identity and DDMI pages for existing SFP/phylink tooling.
-
-  This is intended as a compatibility path for existing device trees. New
-  fixed-front-end consumers should use the optical frontend API directly.
-
 endif

+source "drivers/net/optical/airoha/Kconfig"
 source "drivers/net/optical/semtech/Kconfig"

 endmenu
diff --git a/drivers/net/optical/Makefile b/drivers/net/optical/Makefile
index 2c02020b41b9..abffc2d60030 100644
--- a/drivers/net/optical/Makefile
+++ b/drivers/net/optical/Makefile
@@ -2,6 +2,6 @@
 obj-$(CONFIG_OPTICAL_FRONTEND) += optical_frontend.o
 optical_frontend-y := core.o
 optical_frontend-$(CONFIG_OPTICAL_FRONTEND_HWMON) += hwmon.o
-optical_frontend-$(CONFIG_OPTICAL_FRONTEND_SFP_COMPAT) += sfp_compat.o

+obj-y += airoha/
 obj-y += semtech/
diff --git a/drivers/net/optical/core.c b/drivers/net/optical/core.c
index 040e937c127c..f9c7d1dcd0c8 100644
--- a/drivers/net/optical/core.c
+++ b/drivers/net/optical/core.c
@@ -13,6 +13,7 @@
 #include <linux/device/class.h>
 #include <linux/device/devres.h>
 #include <linux/err.h>
+#include <linux/gpio/consumer.h>
 #include <linux/idr.h>
 #include <linux/jiffies.h>
 #include <linux/module.h>
@@ -22,6 +23,7 @@
 #include <linux/property.h>
 #include <linux/slab.h>
 #include <linux/string.h>
+#include <linux/sysfs.h>

 struct optical_frontend {
  struct device dev;
@@ -29,6 +31,7 @@ struct optical_frontend {
  const struct optical_frontend_desc *desc;
  const struct optical_frontend_ops *ops;
  void *drvdata;
+ struct gpio_desc *tx_disable_gpio;

  struct mutex op_lock; /* serializes provider callbacks and shared state */
  struct optical_frontend_mode mode;
@@ -43,6 +46,215 @@ struct optical_frontend {

 static DEFINE_IDA(optical_frontend_ida);

+static const char *optical_frontend_protocol_name(enum
optical_frontend_protocol protocol)
+{
+ switch (protocol) {
+ case OPTICAL_FRONTEND_PROTO_ETHERNET:
+ return "ethernet";
+ case OPTICAL_FRONTEND_PROTO_EPON:
+ return "epon";
+ case OPTICAL_FRONTEND_PROTO_GPON:
+ return "gpon";
+ case OPTICAL_FRONTEND_PROTO_XGPON:
+ return "xgpon";
+ case OPTICAL_FRONTEND_PROTO_XGSPON:
+ return "xgspon";
+ case OPTICAL_FRONTEND_PROTO_NGPON2:
+ return "ngpon2";
+ case OPTICAL_FRONTEND_PROTO_UNSPEC:
+ default:
+ return "unspecified";
+ }
+}
+
+static ssize_t vendor_show(struct device *dev, struct device_attribute *attr,
+   char *buf)
+{
+ struct optical_frontend *frontend =
+ container_of(dev, struct optical_frontend, dev);
+
+ return sysfs_emit(buf, "%s\n",
+  frontend->desc->vendor_name ?: "unknown");
+}
+static DEVICE_ATTR_RO(vendor);
+
+static ssize_t model_show(struct device *dev, struct device_attribute *attr,
+  char *buf)
+{
+ struct optical_frontend *frontend =
+ container_of(dev, struct optical_frontend, dev);
+
+ return sysfs_emit(buf, "%s\n",
+  frontend->desc->part_number ?: frontend->desc->name);
+}
+static DEVICE_ATTR_RO(model);
+
+static ssize_t type_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct optical_frontend *frontend =
+ container_of(dev, struct optical_frontend, dev);
+
+ return sysfs_emit(buf, "%s\n", frontend->desc->type ?: "unknown");
+}
+static DEVICE_ATTR_RO(type);
+
+static ssize_t capabilities_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct optical_frontend *frontend =
+ container_of(dev, struct optical_frontend, dev);
+ u32 capabilities = frontend->desc->capabilities;
+
+ if (frontend->tx_disable_gpio)
+ capabilities |= OPTICAL_FRONTEND_CAP_TX_DISABLE;
+
+ return sysfs_emit(buf, "0x%08x\n", capabilities);
+}
+static DEVICE_ATTR_RO(capabilities);
+
+static ssize_t supported_protocols_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct optical_frontend *frontend =
+ container_of(dev, struct optical_frontend, dev);
+ ssize_t len = 0;
+ unsigned int protocol;
+
+ for (protocol = OPTICAL_FRONTEND_PROTO_ETHERNET;
+     protocol <= OPTICAL_FRONTEND_PROTO_NGPON2; protocol++) {
+ if (!(frontend->desc->protocols & BIT(protocol)))
+ continue;
+ len += sysfs_emit_at(buf, len, "%s%s", len ? " " : "",
+     optical_frontend_protocol_name(protocol));
+ }
+
+ return sysfs_emit_at(buf, len, "\n");
+}
+static DEVICE_ATTR_RO(supported_protocols);
+
+static ssize_t protocol_show(struct device *dev, struct device_attribute *attr,
+     char *buf)
+{
+ struct optical_frontend *frontend =
+ container_of(dev, struct optical_frontend, dev);
+ struct optical_frontend_mode mode;
+ int ret;
+
+ ret = optical_frontend_get_mode(frontend, &mode);
+ if (ret == -ENODATA)
+ return sysfs_emit(buf, "unspecified\n");
+ if (ret)
+ return ret;
+
+ return sysfs_emit(buf, "%s\n", optical_frontend_protocol_name(mode.protocol));
+}
+static DEVICE_ATTR_RO(protocol);
+
+static int optical_frontend_sysfs_state(struct optical_frontend *frontend,
+ struct optical_frontend_state *state)
+{
+ int ret = optical_frontend_get_state(frontend, state);
+
+ return ret == -EOPNOTSUPP ? -ENODATA : ret;
+}
+
+#define OPTICAL_FRONTEND_STATE_ATTR(_name, _flag, _member) \
+static ssize_t _name##_show(struct device *dev, \
+    struct device_attribute *attr, char *buf) \
+{ \
+ struct optical_frontend *frontend = \
+ container_of(dev, struct optical_frontend, dev); \
+ struct optical_frontend_state state; \
+ int ret; \
+\
+ ret = optical_frontend_sysfs_state(frontend, &state); \
+ if (ret) \
+ return ret; \
+ if (!(state.valid & (_flag))) \
+ return -ENODATA; \
+ return sysfs_emit(buf, "%u\n", state._member); \
+} \
+static DEVICE_ATTR_RO(_name)
+
+OPTICAL_FRONTEND_STATE_ATTR(present,
OPTICAL_FRONTEND_STATE_F_PRESENT, present);
+OPTICAL_FRONTEND_STATE_ATTR(ready, OPTICAL_FRONTEND_STATE_F_READY, ready);
+OPTICAL_FRONTEND_STATE_ATTR(rx_los, OPTICAL_FRONTEND_STATE_F_RX_LOS, rx_los);
+OPTICAL_FRONTEND_STATE_ATTR(tx_fault,
OPTICAL_FRONTEND_STATE_F_TX_FAULT, tx_fault);
+OPTICAL_FRONTEND_STATE_ATTR(tx_enabled, OPTICAL_FRONTEND_STATE_F_TX_ENABLED,
+    tx_enabled);
+
+static ssize_t alarms_show(struct device *dev, struct device_attribute *attr,
+   char *buf)
+{
+ struct optical_frontend *frontend =
+ container_of(dev, struct optical_frontend, dev);
+ struct optical_frontend_telemetry telemetry;
+ int ret;
+
+ ret = optical_frontend_get_telemetry(frontend, &telemetry);
+ if (ret)
+ return ret;
+ if (!(telemetry.valid & OPTICAL_FRONTEND_TELEMETRY_F_ALARMS))
+ return -ENODATA;
+
+ return sysfs_emit(buf, "0x%08x\n", telemetry.alarms);
+}
+static DEVICE_ATTR_RO(alarms);
+
+static struct attribute *optical_frontend_attrs[] = {
+ &dev_attr_vendor.attr,
+ &dev_attr_model.attr,
+ &dev_attr_type.attr,
+ &dev_attr_capabilities.attr,
+ &dev_attr_supported_protocols.attr,
+ &dev_attr_protocol.attr,
+ &dev_attr_present.attr,
+ &dev_attr_ready.attr,
+ &dev_attr_rx_los.attr,
+ &dev_attr_tx_fault.attr,
+ &dev_attr_tx_enabled.attr,
+ &dev_attr_alarms.attr,
+ NULL,
+};
+
+static umode_t optical_frontend_attr_is_visible(struct kobject *kobj,
+ struct attribute *attr, int n)
+{
+ struct device *dev = kobj_to_dev(kobj);
+ struct optical_frontend *frontend =
+ container_of(dev, struct optical_frontend, dev);
+
+ if (attr == &dev_attr_present.attr || attr == &dev_attr_ready.attr)
+ return frontend->ops->get_state ? 0444 : 0;
+ if (attr == &dev_attr_rx_los.attr)
+ return frontend->desc->capabilities & OPTICAL_FRONTEND_CAP_RX_LOS ?
+ 0444 : 0;
+ if (attr == &dev_attr_tx_fault.attr)
+ return frontend->desc->capabilities & OPTICAL_FRONTEND_CAP_TX_FAULT ?
+ 0444 : 0;
+ if (attr == &dev_attr_tx_enabled.attr)
+ return frontend->tx_disable_gpio || frontend->ops->tx_enable ||
+       (frontend->desc->capabilities & OPTICAL_FRONTEND_CAP_TX_DISABLE) ?
+ 0444 : 0;
+ if (attr == &dev_attr_alarms.attr)
+ return frontend->desc->capabilities & OPTICAL_FRONTEND_CAP_ALARMS ?
+ 0444 : 0;
+
+ return 0444;
+}
+
+static const struct attribute_group optical_frontend_group = {
+ .attrs = optical_frontend_attrs,
+ .is_visible = optical_frontend_attr_is_visible,
+};
+
+static const struct attribute_group *optical_frontend_groups[] = {
+ &optical_frontend_group,
+ NULL,
+};
+
 static void optical_frontend_dev_release(struct device *dev)
 {
  struct optical_frontend *frontend =
@@ -55,12 +267,15 @@ static void optical_frontend_dev_release(struct
device *dev)
 static const struct class optical_frontend_class = {
  .name = "optical_frontend",
  .dev_release = optical_frontend_dev_release,
+ .dev_groups = optical_frontend_groups,
 };

 static void optical_frontend_unregister(void *data)
 {
  struct optical_frontend *frontend = data;

+ if (frontend->tx_disable_gpio)
+ gpiod_set_value_cansleep(frontend->tx_disable_gpio, 1);
  device_unregister(&frontend->dev);
 }

@@ -78,11 +293,20 @@ devm_optical_frontend_register(struct device *dev,
        void *drvdata)
 {
  struct optical_frontend *frontend;
+ struct gpio_desc *tx_disable_gpio;
  int ret;

  if (!dev || !desc || !desc->name || !ops)
  return ERR_PTR(-EINVAL);

+ tx_disable_gpio = devm_gpiod_get_optional(dev, "tx-disable",
+  GPIOD_OUT_HIGH);
+ if (IS_ERR(tx_disable_gpio)) {
+ ret = dev_err_probe(dev, PTR_ERR(tx_disable_gpio),
+    "failed to get TX disable GPIO\n");
+ return ERR_PTR(ret);
+ }
+
  frontend = kzalloc(sizeof(*frontend), GFP_KERNEL);
  if (!frontend)
  return ERR_PTR(-ENOMEM);
@@ -98,11 +322,13 @@ devm_optical_frontend_register(struct device *dev,
  frontend->desc = desc;
  frontend->ops = ops;
  frontend->drvdata = drvdata;
+ frontend->tx_disable_gpio = tx_disable_gpio;
  mutex_init(&frontend->op_lock);

  device_initialize(&frontend->dev);
  frontend->dev.class = &optical_frontend_class;
  frontend->dev.parent = dev;
+ frontend->dev.groups = desc->groups;
  device_set_node(&frontend->dev, dev_fwnode(dev));
  dev_set_name(&frontend->dev, "frontend%d", frontend->id);

@@ -229,6 +455,21 @@ void *optical_frontend_get_drvdata(struct
optical_frontend *frontend)
 }
 EXPORT_SYMBOL_GPL(optical_frontend_get_drvdata);

+struct optical_frontend *optical_frontend_from_dev(struct device *dev)
+{
+ if (!dev || dev->class != &optical_frontend_class)
+ return NULL;
+
+ return container_of(dev, struct optical_frontend, dev);
+}
+EXPORT_SYMBOL_GPL(optical_frontend_from_dev);
+
+struct device *optical_frontend_get_device(struct optical_frontend *frontend)
+{
+ return frontend ? &frontend->dev : NULL;
+}
+EXPORT_SYMBOL_GPL(optical_frontend_get_device);
+
 struct device *optical_frontend_get_provider(struct optical_frontend *frontend)
 {
  return frontend ? frontend->provider : NULL;
@@ -286,6 +527,39 @@ int optical_frontend_get_mode(struct
optical_frontend *frontend,
 }
 EXPORT_SYMBOL_GPL(optical_frontend_get_mode);

+int optical_frontend_tx_enable(struct optical_frontend *frontend, bool enable)
+{
+ int ret = 0;
+
+ if (!frontend)
+ return -EINVAL;
+ if (!frontend->tx_disable_gpio && !frontend->ops->tx_enable)
+ return -EOPNOTSUPP;
+
+ mutex_lock(&frontend->op_lock);
+
+ /* Block the optical output before asking the provider to shut down. */
+ if (!enable && frontend->tx_disable_gpio)
+ gpiod_set_value_cansleep(frontend->tx_disable_gpio, 1);
+
+ if (frontend->ops->tx_enable) {
+ ret = frontend->ops->tx_enable(frontend, enable);
+ if (ret)
+ goto out;
+ }
+
+ /* Release the external interlock only after the provider is ready. */
+ if (enable && frontend->tx_disable_gpio)
+ gpiod_set_value_cansleep(frontend->tx_disable_gpio, 0);
+
+out:
+ frontend->telemetry_cache_valid = false;
+ mutex_unlock(&frontend->op_lock);
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(optical_frontend_tx_enable);
+
 int optical_frontend_tx_rearm(struct optical_frontend *frontend)
 {
  int ret;
@@ -307,18 +581,39 @@ EXPORT_SYMBOL_GPL(optical_frontend_tx_rearm);
 int optical_frontend_get_state(struct optical_frontend *frontend,
        struct optical_frontend_state *state)
 {
- int ret;
+ int gpio_value;
+ int ret = 0;

  if (!frontend || !state)
  return -EINVAL;
- if (!frontend->ops->get_state)
- return -EOPNOTSUPP;

  memset(state, 0, sizeof(*state));
  mutex_lock(&frontend->op_lock);
- ret = frontend->ops->get_state(frontend, state);
- mutex_unlock(&frontend->op_lock);
+ if (frontend->ops->get_state) {
+ ret = frontend->ops->get_state(frontend, state);
+ if (ret)
+ goto out;
+ }

+ /* TX_DISABLE is a core-owned interlock, so report it even when the
+ * provider has no matching status register. GPIO values are logical:
+ * one means disabled regardless of the electrical active level.
+ */
+ if (frontend->tx_disable_gpio) {
+ gpio_value = gpiod_get_value_cansleep(frontend->tx_disable_gpio);
+ if (gpio_value < 0) {
+ ret = gpio_value;
+ goto out;
+ }
+ state->tx_enabled = !gpio_value;
+ state->valid |= OPTICAL_FRONTEND_STATE_F_TX_ENABLED;
+ }
+
+ if (!state->valid)
+ ret = -ENODATA;
+
+out:
+ mutex_unlock(&frontend->op_lock);
  return ret;
 }
 EXPORT_SYMBOL_GPL(optical_frontend_get_state);
diff --git a/drivers/net/optical/hwmon.c b/drivers/net/optical/hwmon.c
index 25645f59e315..54b251d9b287 100644
--- a/drivers/net/optical/hwmon.c
+++ b/drivers/net/optical/hwmon.c
@@ -6,6 +6,7 @@
 #include <linux/kernel.h>
 #include <linux/module.h>
 #include <linux/optical_frontend.h>
+#include <linux/sysfs.h>

 static bool optical_frontend_hwmon_has_threshold(
  const struct optical_frontend_desc *desc,
@@ -285,25 +286,46 @@ static const struct hwmon_chip_info
optical_frontend_hwmon_chip_info = {
  .info = optical_frontend_hwmon_info,
 };

+static void optical_frontend_hwmon_remove_link(void *data)
+{
+ struct optical_frontend *frontend = data;
+ struct device *dev = optical_frontend_get_device(frontend);
+
+ if (dev)
+ sysfs_remove_link(&dev->kobj, "hwmon");
+}
+
 int devm_optical_frontend_hwmon_register(struct optical_frontend *frontend)
 {
  const struct optical_frontend_desc *desc;
+ struct device *frontend_dev;
  struct device *provider;
  struct device *hwmon;
+ int ret;

  if (!frontend)
  return -EINVAL;

  desc = optical_frontend_get_desc(frontend);
+ frontend_dev = optical_frontend_get_device(frontend);
  provider = optical_frontend_get_provider(frontend);
- if (!desc || !provider)
+ if (!desc || !frontend_dev || !provider)
  return -EINVAL;

  hwmon = devm_hwmon_device_register_with_info(provider, desc->name,
      frontend,
      &optical_frontend_hwmon_chip_info,
      NULL);
- return PTR_ERR_OR_ZERO(hwmon);
+ if (IS_ERR(hwmon))
+ return PTR_ERR(hwmon);
+
+ ret = sysfs_create_link(&frontend_dev->kobj, &hwmon->kobj, "hwmon");
+ if (ret)
+ return ret;
+
+ return devm_add_action_or_reset(provider,
+ optical_frontend_hwmon_remove_link,
+ frontend);
 }
 EXPORT_SYMBOL_GPL(devm_optical_frontend_hwmon_register);

diff --git a/include/linux/optical_frontend.h b/include/linux/optical_frontend.h
index aee25e92cd3d..426140e30545 100644
--- a/include/linux/optical_frontend.h
+++ b/include/linux/optical_frontend.h
@@ -8,6 +8,7 @@
 #include <linux/kconfig.h>
 #include <linux/types.h>

+struct attribute_group;
 struct device;
 struct fwnode_handle;
 struct optical_frontend;
@@ -152,6 +153,7 @@ struct optical_frontend_thresholds {
 /**
  * struct optical_frontend_desc - immutable provider description
  * @name: short driver/model name used by hwmon and diagnostics
+ * @type: frontend type string exposed through sysfs (for example "lddla")
  * @vendor_name: vendor string for SFF compatibility
  * @vendor_oui: vendor OUI for SFF compatibility
  * @part_number: part number for SFF compatibility
@@ -161,9 +163,11 @@ struct optical_frontend_thresholds {
  * @protocols: BIT(OPTICAL_FRONTEND_PROTO_*) bitmap
  * @thresholds: optional normalized alarm thresholds
  * @telemetry_cache_ms: core telemetry cache lifetime; zero disables caching
+ * @groups: optional provider-specific sysfs groups on the frontend
class device
  */
 struct optical_frontend_desc {
  const char *name;
+ const char *type;
  const char *vendor_name;
  u8 vendor_oui[3];
  const char *part_number;
@@ -173,11 +177,13 @@ struct optical_frontend_desc {
  u32 protocols;
  const struct optical_frontend_thresholds *thresholds;
  u32 telemetry_cache_ms;
+ const struct attribute_group **groups;
 };

 /**
  * struct optical_frontend_ops - provider operations
  * @set_mode: configure protocol/rates; optional
+ * @tx_enable: prepare/enable or disable the optical transmitter; optional
  * @tx_rearm: rearm a transmitter safety/fault latch; optional
  * @get_state: retrieve normalized frontend state; optional
  * @get_telemetry: retrieve normalized live measurements; optional
@@ -188,6 +194,7 @@ struct optical_frontend_desc {
 struct optical_frontend_ops {
  int (*set_mode)(struct optical_frontend *frontend,
  const struct optical_frontend_mode *mode);
+ int (*tx_enable)(struct optical_frontend *frontend, bool enable);
  int (*tx_rearm)(struct optical_frontend *frontend);
  int (*get_state)(struct optical_frontend *frontend,
  struct optical_frontend_state *state);
@@ -209,6 +216,8 @@ devm_optical_frontend_get_by_fwnode(struct device *dev,
     const struct fwnode_handle *fwnode);

 void *optical_frontend_get_drvdata(struct optical_frontend *frontend);
+struct optical_frontend *optical_frontend_from_dev(struct device *dev);
+struct device *optical_frontend_get_device(struct optical_frontend *frontend);
 struct device *optical_frontend_get_provider(struct optical_frontend
*frontend);
 const struct optical_frontend_desc *
 optical_frontend_get_desc(struct optical_frontend *frontend);
@@ -217,6 +226,7 @@ int optical_frontend_set_mode(struct
optical_frontend *frontend,
       const struct optical_frontend_mode *mode);
 int optical_frontend_get_mode(struct optical_frontend *frontend,
       struct optical_frontend_mode *mode);
+int optical_frontend_tx_enable(struct optical_frontend *frontend, bool enable);
 int optical_frontend_tx_rearm(struct optical_frontend *frontend);
 int optical_frontend_get_state(struct optical_frontend *frontend,
        struct optical_frontend_state *state);
@@ -251,6 +261,18 @@ static inline void
*optical_frontend_get_drvdata(struct optical_frontend *fronte
  return NULL;
 }

+static inline struct optical_frontend *
+optical_frontend_from_dev(struct device *dev)
+{
+ return NULL;
+}
+
+static inline struct device *
+optical_frontend_get_device(struct optical_frontend *frontend)
+{
+ return NULL;
+}
+
 static inline struct device *
 optical_frontend_get_provider(struct optical_frontend *frontend)
 {
@@ -277,6 +299,12 @@ optical_frontend_get_mode(struct optical_frontend
*frontend,
  return -EOPNOTSUPP;
 }

+static inline int
+optical_frontend_tx_enable(struct optical_frontend *frontend, bool enable)
+{
+ return -EOPNOTSUPP;
+}
+
 static inline int optical_frontend_tx_rearm(struct optical_frontend *frontend)
 {
  return -EOPNOTSUPP;
@@ -313,14 +341,4 @@ devm_optical_frontend_hwmon_register(struct
optical_frontend *frontend)
 }
 #endif

-#if IS_REACHABLE(CONFIG_OPTICAL_FRONTEND_SFP_COMPAT)
-int devm_optical_frontend_sfp_register(struct optical_frontend *frontend);
-#else
-static inline int
-devm_optical_frontend_sfp_register(struct optical_frontend *frontend)
-{
- return 0;
-}
-#endif
-
 #endif /* _LINUX_OPTICAL_FRONTEND_H */
diff --git a/include/net/xpon/oam.h b/include/net/xpon/oam.h
new file mode 100644
index 000000000000..559a837222ff
--- /dev/null
+++ b/include/net/xpon/oam.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _NET_XPON_OAM_H
+#define _NET_XPON_OAM_H
+
+#include <linux/types.h>
+
+struct sk_buff;
+struct xpon_device;
+struct xpon_oam;
+
+#define XPON_OAM_MAX_LLID 8
+
+struct xpon_oam_ops {
+ int (*xmit)(struct xpon_oam *oam, struct sk_buff *skb, u8 llid_index);
+};
+
+struct xpon_oam *xpon_oam_register(struct xpon_device *xpon,
+   const struct xpon_oam_ops *ops,
+   void *priv);
+void xpon_oam_unregister(struct xpon_oam *oam);
+void *xpon_oam_priv(struct xpon_oam *oam);
+
+void xpon_oam_llid_registered(struct xpon_oam *oam, u8 index, u16 llid);
+void xpon_oam_llid_unregistered(struct xpon_oam *oam, u8 index);
+void xpon_oam_receive(struct xpon_oam *oam, struct sk_buff *skb,
+      u8 llid_index);
+
+#endif /* _NET_XPON_OAM_H */
diff --git a/include/net/xpon/omci.h b/include/net/xpon/omci.h
new file mode 100644
index 000000000000..7f38d9bef161
--- /dev/null
+++ b/include/net/xpon/omci.h
@@ -0,0 +1,364 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _NET_OMCI_H
+#define _NET_OMCI_H
+
+#include <linux/bits.h>
+#include <linux/types.h>
+#include <uapi/linux/omci.h>
+
+struct device;
+struct omci_device;
+struct xpon_device;
+struct sk_buff;
+
+#define OMCI_IDENTITY_F_SERIAL_NUMBER BIT(0)
+#define OMCI_IDENTITY_F_VENDOR_ID BIT(1)
+#define OMCI_IDENTITY_F_PASSWORD BIT(2)
+#define OMCI_IDENTITY_F_VERSION BIT(3)
+#define OMCI_IDENTITY_F_EQUIPMENT_ID BIT(4)
+
+/**
+ * struct omci_identity - normalized ONU identity and registration data
+ * @valid: OMCI_IDENTITY_F_* bitmap
+ * @serial_number: canonical eight-byte GPON serial number
+ * @vendor_id: canonical four-byte vendor identifier
+ * @password: canonical ten-byte GPON registration password
+ * @version: ONU-G version field
+ * @equipment_id: ONU2-G equipment identifier
+ * @serial_source: enum omci_config_source for @serial_number
+ * @vendor_source: enum omci_config_source for @vendor_id
+ * @password_source: enum omci_config_source for @password
+ * @version_source: enum omci_config_source for @version
+ * @equipment_source: enum omci_config_source for @equipment_id
+ */
+struct omci_identity {
+ u32 valid;
+ u8 serial_number[8];
+ u8 vendor_id[OMCI_OLT_VENDOR_ID_LEN];
+ u8 password[10];
+ u8 version[OMCI_OLT_VERSION_LEN];
+ u8 equipment_id[OMCI_OLT_EQUIPMENT_ID_LEN];
+ u8 serial_source;
+ u8 vendor_source;
+ u8 password_source;
+ u8 version_source;
+ u8 equipment_source;
+};
+
+/**
+ * struct omci_me_class - managed entity class descriptor
+ * @class_id: ITU managed entity class identifier
+ * @category: enum omci_class_category
+ * @support: enum omci_class_support
+ * @flags: OMCI_CLASS_F_* bitmap
+ * @name: stable human-readable managed entity name
+ */
+struct omci_me_class {
+ u16 class_id;
+ u8 category;
+ u8 support;
+ u32 flags;
+ const char *name;
+};
+
+enum omci_gem_port_direction {
+ OMCI_GEM_PORT_DIRECTION_UNI_TO_ANI = 1,
+ OMCI_GEM_PORT_DIRECTION_ANI_TO_UNI = 2,
+ OMCI_GEM_PORT_DIRECTION_BIDIRECTIONAL = 3,
+};
+
+/**
+ * struct omci_olt_g - parsed OLT-G identification attributes
+ * @vendor_id: four-character OLT vendor identifier
+ * @equipment_id: OLT equipment or model identifier
+ * @version: OLT software or hardware version identifier
+ * @vendor_id_valid: @vendor_id has been received from the OLT
+ * @equipment_id_valid: @equipment_id has been received from the OLT
+ * @version_valid: @version has been received from the OLT
+ * @valid: at least one identification attribute is available
+ */
+struct omci_olt_g {
+ char vendor_id[OMCI_OLT_VENDOR_ID_LEN + 1];
+ char equipment_id[OMCI_OLT_EQUIPMENT_ID_LEN + 1];
+ char version[OMCI_OLT_VERSION_LEN + 1];
+ bool vendor_id_valid;
+ bool equipment_id_valid;
+ bool version_valid;
+ bool valid;
+};
+
+/**
+ * struct omci_olt_profile_state - resolved OLT interoperability policy
+ * @configured: user-selected profile, including auto
+ * @effective: active concrete profile
+ * @forced: forced concrete profile, or OMCI_OLT_PROFILE_UNSPEC
+ * @quirks: OMCI_OLT_QUIRK_* bitmap for @effective
+ * @olt: latest parsed OLT-G identity
+ */
+struct omci_olt_profile_state {
+ u8 configured;
+ u8 effective;
+ u8 forced;
+ u32 quirks;
+ struct omci_olt_g olt;
+};
+
+/**
+ * struct omci_vlan_filter_entry - parsed VLAN tagging filter entry
+ * @tci: original VLAN tag control information value
+ * @vid: VLAN identifier
+ * @pbit: priority code point
+ * @dei: drop eligible indicator
+ */
+struct omci_vlan_filter_entry {
+ u16 tci;
+ u16 vid;
+ u8 pbit;
+ u8 dei;
+};
+
+/**
+ * struct omci_vlan_tagging_filter - parsed class 84 attributes
+ * @entries: valid VLAN filter entries
+ * @forward_operation: forwarding operation from the managed entity
+ * @num_entries: number of valid entries in @entries
+ * @valid: parsed data is available
+ */
+struct omci_vlan_tagging_filter {
+ struct omci_vlan_filter_entry entries[OMCI_VLAN_FILTER_MAX_ENTRIES];
+ u8 forward_operation;
+ u8 num_entries;
+ bool valid;
+};
+
+/**
+ * struct omci_extended_vlan_rule - parsed class 171 table entry
+ * @raw: original 16-byte table entry
+ * @filter_outer_vid: outer-tag VLAN identifier match
+ * @filter_inner_vid: inner-tag VLAN identifier match
+ * @treat_outer_vid: outer-tag VLAN identifier treatment
+ * @treat_inner_vid: inner-tag VLAN identifier treatment
+ * @filter_outer_pbit: outer-tag priority match
+ * @filter_outer_tpid_dei: outer-tag TPID/DEI match mode
+ * @filter_inner_pbit: inner-tag priority match
+ * @filter_inner_tpid_dei: inner-tag TPID/DEI match mode
+ * @filter_ethertype: EtherType match mode
+ * @tags_to_remove: number of tags removed, or discard mode
+ * @treat_outer_pbit: outer-tag priority treatment
+ * @treat_outer_tpid_dei: outer-tag TPID/DEI treatment mode
+ * @treat_inner_pbit: inner-tag priority treatment
+ * @treat_inner_tpid_dei: inner-tag TPID/DEI treatment mode
+ * @delete: entry encodes a table deletion
+ */
+struct omci_extended_vlan_rule {
+ u8 raw[OMCI_EXT_VLAN_RULE_LEN];
+ u16 filter_outer_vid;
+ u16 filter_inner_vid;
+ u16 treat_outer_vid;
+ u16 treat_inner_vid;
+ u8 filter_outer_pbit;
+ u8 filter_outer_tpid_dei;
+ u8 filter_inner_pbit;
+ u8 filter_inner_tpid_dei;
+ u8 filter_ethertype;
+ u8 tags_to_remove;
+ u8 treat_outer_pbit;
+ u8 treat_outer_tpid_dei;
+ u8 treat_inner_pbit;
+ u8 treat_inner_tpid_dei;
+ bool delete;
+};
+
+/**
+ * struct omci_extended_vlan - parsed class 171 attributes and table
+ * @rules: parsed VLAN operation table
+ * @dscp_to_pbit: DSCP-to-P-bit mapping attribute
+ * @input_tpid: input TPID
+ * @output_tpid: output TPID
+ * @associated_me: associated managed entity pointer
+ * @max_table_size: advertised maximum number of table entries
+ * @association_type: associated managed entity type
+ * @downstream_mode: downstream VLAN processing mode
+ * @rule_count: number of valid rules in @rules
+ * @valid: parsed data is available
+ */
+struct omci_extended_vlan {
+ struct omci_extended_vlan_rule rules[OMCI_EXT_VLAN_MAX_RULES];
+ u8 dscp_to_pbit[24];
+ u16 input_tpid;
+ u16 output_tpid;
+ u16 associated_me;
+ u16 max_table_size;
+ u8 association_type;
+ u8 downstream_mode;
+ u8 rule_count;
+ bool valid;
+};
+
+/**
+ * struct omci_telemetry - current PON and optical telemetry
+ * @valid: OMCI_TELEMETRY_F_* bitmap
+ * @bosa_temperature_mc: BOSA temperature in milli-degrees Celsius
+ * @bosa_voltage_uv: optical frontend supply voltage in microvolts
+ * @bosa_bias_ua: laser bias current in microamps
+ * @bosa_tx_power_nw: transmitted optical power in nanowatts
+ * @bosa_rx_power_nw: received optical power in nanowatts
+ * @bosa_alarms: hardware-specific optical alarm bitmap
+ * @downstream_fec: enum omci_fec_status
+ * @upstream_fec: enum omci_fec_status
+ */
+struct omci_telemetry {
+ u32 valid;
+ s32 bosa_temperature_mc;
+ u32 bosa_voltage_uv;
+ u32 bosa_bias_ua;
+ u32 bosa_tx_power_nw;
+ u32 bosa_rx_power_nw;
+ u32 bosa_alarms;
+ u8 downstream_fec;
+ u8 upstream_fec;
+};
+
+/**
+ * struct omci_ani_topology - pre-existing upstream traffic resources
+ * @tcont_base: first T-CONT managed entity ID
+ * @scheduler_base: first Traffic Scheduler managed entity ID
+ * @queue_base: first Priority Queue managed entity ID
+ * @maximum_queue_size: maximum queue size reported in 2048-byte blocks
+ * @allocated_queue_size: initial allocated queue size in 2048-byte blocks
+ * @tcont_count: number of upstream T-CONT resources
+ * @queues_per_tcont: number of upstream priority queues per T-CONT
+ * @queue_config_option: Priority Queue configuration option
+ * @scheduler_policy: initial Traffic Scheduler policy
+ *
+ * The OMCI agent uses this description to seed complete Priority Queue and
+ * Traffic Scheduler managed entities before the first MIB upload. Entity IDs
+ * are allocated contiguously. Queue entity IDs are grouped by T-CONT.
+ */
+/**
+ * struct omci_service_config - normalized upstream OMCI service
+ * @cookie: stable identifier used to replace or delete one service rule
+ * @uni_entity_id: normalized PPTP Ethernet UNI or VEIP entity
+ * @gem_ctp_entity_id: GEM port network CTP managed entity
+ * @gem_port_id: GEM port identifier programmed in the GPON MAC
+ * @tcont_entity_id: T-CONT managed entity associated with the GEM port
+ * @alloc_id: Alloc-ID of that T-CONT, so a backend whose entity map was
+ * cleared by a GPON restart can still resolve the channel
+ * @vlan_id: VLAN identifier used for upstream classification
+ * @pcp: IEEE 802.1p priority used for classification and queue selection
+ * @queue: hardware upstream queue
+ * @direction: enum omci_gem_port_direction
+ * @vlan_treatment: raw class 171 treatment words for the backend
+ * @multicast_ani_entity_id: Dasan WAN bridge port used as multicast ANI
+ * @vlan_valid: @vlan_id participates in classification
+ * @pcp_valid: @pcp participates in classification
+ * @vlan_treatment_valid: @vlan_treatment requires backend processing
+ * @multicast_ani_valid: @multicast_ani_entity_id was profile-resolved
+ * @multicast: service represents a multicast GEM path
+ * @default_service: fallback service when no VLAN/PCP rule matches
+ */
+struct omci_service_config {
+ u32 cookie;
+ u16 uni_entity_id;
+ u16 gem_ctp_entity_id;
+ u16 gem_port_id;
+ u16 tcont_entity_id;
+ u16 alloc_id;
+ u16 vlan_id;
+ u8 pcp;
+ u8 queue;
+ u8 direction;
+ u8 vlan_treatment[8];
+ u16 multicast_ani_entity_id;
+ bool vlan_valid;
+ bool pcp_valid;
+ bool vlan_treatment_valid;
+ bool multicast_ani_valid;
+ bool multicast;
+ bool default_service;
+};
+
+struct omci_ani_topology {
+ u16 tcont_base;
+ u16 scheduler_base;
+ u16 queue_base;
+ u16 maximum_queue_size;
+ u16 allocated_queue_size;
+ u8 tcont_count;
+ u8 queues_per_tcont;
+ u8 queue_config_option;
+ u8 scheduler_policy;
+};
+
+/**
+ * struct omci_device_ops - hardware transport and provisioning operations
+ * @start: acquire and start the OMCI transport independently of netdev state
+ * @stop: stop and release the OMCI transport
+ * @xmit: transmit an OMCI PDU; consumes @skb only on success
+ * @get_ani_topology: describe pre-existing ANI scheduling resources
+ * @set_tcont: configure a T-CONT mapping
+ * @set_gem_port: configure a GEM port
+ * @set_uni: enable or disable a UNI
+ * @replace_service: atomically add or replace a normalized upstream service
+ * @delete_service: remove a normalized upstream service by cookie
+ * @get_telemetry: refresh PON FEC and optical telemetry
+ * @set_olt_profile: apply a resolved OLT interoperability profile
+ * @set_operational: report whether the in-kernel OMCI agent is operational
+ * @config_changed: report a normalized runtime OMCI configuration change
+ */
+struct omci_device_ops {
+ int (*start)(struct omci_device *odev);
+ void (*stop)(struct omci_device *odev);
+ int (*xmit)(struct omci_device *odev, struct sk_buff *skb,
+    u16 gem_port_id);
+ int (*get_ani_topology)(struct omci_device *odev,
+ struct omci_ani_topology *topology);
+ int (*set_tcont)(struct omci_device *odev, u16 entity_id,
+ u16 alloc_id, bool valid);
+ int (*set_gem_port)(struct omci_device *odev, u16 entity_id,
+    u16 gem_port_id, u16 tcont_entity_id,
+    u8 direction, bool valid, bool encrypted);
+ int (*set_uni)(struct omci_device *odev, u16 entity_id, bool enable);
+ int (*replace_service)(struct omci_device *odev,
+       const struct omci_service_config *service);
+ int (*delete_service)(struct omci_device *odev, u32 cookie);
+ int (*get_telemetry)(struct omci_device *odev,
+     struct omci_telemetry *telemetry);
+ int (*set_olt_profile)(struct omci_device *odev,
+       const struct omci_olt_profile_state *state);
+ void (*set_operational)(struct omci_device *odev, bool operational);
+ void (*config_changed)(struct omci_device *odev, u16 key,
+       const struct omci_identity *identity);
+};
+
+struct omci_device *
+omci_device_register(struct xpon_device *xpon, u32 capabilities,
+     const struct omci_device_ops *ops, void *priv);
+void omci_device_unregister(struct omci_device *odev);
+int omci_device_start(struct omci_device *odev);
+void omci_device_stop(struct omci_device *odev);
+
+void *omci_device_priv(const struct omci_device *odev);
+u32 omci_device_id(const struct omci_device *odev);
+const char *omci_olt_profile_name(u8 profile);
+
+int omci_identity_load(struct device *dev, struct omci_identity *identity);
+void gpon_random_serial_number(const u8 vendor[4], struct
omci_identity *identity);
+void omci_device_set_identity_info(struct omci_device *odev,
+   const struct omci_identity *identity);
+void omci_device_set_identity(struct omci_device *odev,
+      const u8 serial_number[8],
+      const u8 password[10]);
+void omci_device_set_onu_id(struct omci_device *odev, u16 onu_id);
+void omci_device_set_channel(struct omci_device *odev, u16 gem_port_id,
+     bool valid);
+void omci_device_set_state(struct omci_device *odev, u8 state);
+void omci_device_reset_session(struct omci_device *odev);
+int omci_device_set_dying_gasp_enabled(struct omci_device *odev,
+       bool enabled, u8 source);
+int omci_device_send_dying_gasp(struct omci_device *odev);
+void omci_device_receive(struct omci_device *odev, struct sk_buff *skb,
+ u16 gem_port_id, u32 flags);
+int omci_device_reconcile_services(struct omci_device *odev);
+
+#endif /* _NET_OMCI_H */
diff --git a/net/xpon/Kconfig b/net/xpon/Kconfig
new file mode 100644
index 000000000000..f72e6b460066
--- /dev/null
+++ b/net/xpon/Kconfig
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: GPL-2.0-only
+menuconfig XPON
+ tristate "xPON subsystem"
+ depends on NET
+ help
+  Generic Passive Optical Network subsystem. It provides the common
+  representation used by GPON, EPON and XGS-PON devices, including
+  registration state, optical state, carrier, LEDs, sysfs and Generic
+  Netlink notifications.
+
+if XPON
+
+config XPON_OMCI
+ tristate "xPON OMCI agent"
+ default y
+ help
+  In-kernel ONU Management and Control Interface agent used by GPON and
+  XGS-PON providers. The protocol implementation is owned by the xPON
+  subsystem and is attached to one xPON device.
+
+config XPON_OAM
+ tristate "xPON IEEE 802.3ah OAM"
+ default y
+ help
+  IEEE 802.3ah Ethernet OAM support for EPON links. The initial
+  implementation provides the per-LLID OAM object and Information
+  OAMPDU wire definitions. Event, variable and vendor extensions are
+  added on top of this core.
+
+endif # XPON
diff --git a/net/xpon/Makefile b/net/xpon/Makefile
new file mode 100644
index 000000000000..654060432a68
--- /dev/null
+++ b/net/xpon/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_XPON) += xpon.o
+xpon-y := core.o sysfs.o leds.o genl.o
+
+obj-$(CONFIG_XPON_OMCI) += omci/
+obj-$(CONFIG_XPON_OAM) += oam/
diff --git a/net/xpon/core.c b/net/xpon/core.c
new file mode 100644
index 000000000000..c3d3ad0cb378
--- /dev/null
+++ b/net/xpon/core.c
@@ -0,0 +1,340 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Generic xPON core
+ *
+ * GPON, EPON and XGS-PON use different registration and management wire
+ * protocols, but share one datapath object, optical state and set of system
+ * consumers. Protocol providers translate their native state into this core.
+ */
+
+#include <linux/err.h>
+#include <linux/module.h>
+#include <linux/netdevice.h>
+#include <linux/slab.h>
+#include <net/xpon.h>
+
+#include "internal.h"
+
+static LIST_HEAD(xpon_devices);
+static DEFINE_MUTEX(xpon_devices_lock);
+
+static void xpon_notify_work(struct work_struct *work)
+{
+ struct xpon_device *xpon = container_of(work, struct xpon_device,
+       notify_work);
+ struct xpon_notifier_info info;
+ unsigned long flags, changed;
+
+ spin_lock_irqsave(&xpon->state_lock, flags);
+ changed = xpon->pending_events;
+ if (!changed) {
+ spin_unlock_irqrestore(&xpon->state_lock, flags);
+ return;
+ }
+
+ info.xpon = xpon;
+ info.changed = changed;
+ info.old = xpon->notify_old;
+ info.new = xpon->state;
+ xpon->notify_old = xpon->state;
+ xpon->pending_events = 0;
+ spin_unlock_irqrestore(&xpon->state_lock, flags);
+
+ xpon_leds_update(xpon, &info.new);
+ xpon_sysfs_notify(xpon, changed);
+ xpon_genl_notify(xpon, changed);
+ blocking_notifier_call_chain(&xpon->notifier, changed, &info);
+}
+
+static void xpon_queue_event(struct xpon_device *xpon, unsigned long changed)
+{
+ unsigned long flags;
+
+ if (!xpon)
+ return;
+
+ spin_lock_irqsave(&xpon->state_lock, flags);
+ xpon->pending_events |= changed;
+ spin_unlock_irqrestore(&xpon->state_lock, flags);
+ schedule_work(&xpon->notify_work);
+}
+
+struct xpon_device *
+xpon_device_register(struct device *parent,
+     const struct xpon_device_desc *desc)
+{
+ struct xpon_device *xpon;
+ int ret;
+
+ if (!parent || !desc || !desc->netdev ||
+    desc->mode >= __XPON_MODE_MAX ||
+    !(desc->modes & XPON_MODE_CAP(desc->mode)))
+ return ERR_PTR(-EINVAL);
+
+ xpon = kzalloc(sizeof(*xpon), GFP_KERNEL);
+ if (!xpon)
+ return ERR_PTR(-ENOMEM);
+
+ INIT_LIST_HEAD(&xpon->list);
+ spin_lock_init(&xpon->state_lock);
+ mutex_init(&xpon->mode_lock);
+ INIT_WORK(&xpon->notify_work, xpon_notify_work);
+ BLOCKING_INIT_NOTIFIER_HEAD(&xpon->notifier);
+
+ xpon->parent = parent;
+ xpon->netdev = desc->netdev;
+ xpon->optical = desc->optical;
+ xpon->ops = desc->ops;
+ xpon->priv = desc->priv;
+ xpon->modes = desc->modes;
+ xpon->pon_led = desc->pon_led;
+ xpon->los_led = desc->los_led;
+ xpon->fiber_led = desc->fiber_led;
+ xpon->state.mode = desc->mode;
+ xpon->state.registration = XPON_REGISTRATION_DOWN;
+ xpon->state.valid = 0;
+ xpon->notify_old = xpon->state;
+
+ ret = xpon_sysfs_register(xpon);
+ if (ret) {
+ kfree(xpon);
+ return ERR_PTR(ret);
+ }
+
+ mutex_lock(&xpon_devices_lock);
+ list_add_tail(&xpon->list, &xpon_devices);
+ mutex_unlock(&xpon_devices_lock);
+
+ dev_info(parent, "registered xPON device for %s\n", desc->netdev->name);
+ return xpon;
+}
+EXPORT_SYMBOL_GPL(xpon_device_register);
+
+void xpon_device_unregister(struct xpon_device *xpon)
+{
+ if (!xpon)
+ return;
+
+ mutex_lock(&xpon_devices_lock);
+ list_del_init(&xpon->list);
+ mutex_unlock(&xpon_devices_lock);
+ cancel_work_sync(&xpon->notify_work);
+ xpon_sysfs_unregister(xpon);
+ kfree(xpon);
+}
+EXPORT_SYMBOL_GPL(xpon_device_unregister);
+
+struct device *xpon_device_dev(struct xpon_device *xpon)
+{
+ return xpon ? xpon->class_dev : NULL;
+}
+EXPORT_SYMBOL_GPL(xpon_device_dev);
+
+struct net_device *xpon_device_netdev(struct xpon_device *xpon)
+{
+ return xpon ? xpon->netdev : NULL;
+}
+EXPORT_SYMBOL_GPL(xpon_device_netdev);
+
+void *xpon_device_priv(struct xpon_device *xpon)
+{
+ return xpon ? xpon->priv : NULL;
+}
+EXPORT_SYMBOL_GPL(xpon_device_priv);
+
+unsigned long xpon_device_modes(struct xpon_device *xpon)
+{
+ return xpon ? xpon->modes : 0;
+}
+EXPORT_SYMBOL_GPL(xpon_device_modes);
+
+enum xpon_mode xpon_device_mode(struct xpon_device *xpon)
+{
+ return xpon ? READ_ONCE(xpon->state.mode) : XPON_MODE_GPON;
+}
+EXPORT_SYMBOL_GPL(xpon_device_mode);
+
+int xpon_device_set_mode(struct xpon_device *xpon, enum xpon_mode mode)
+{
+ enum xpon_mode old_mode;
+ int ret = 0;
+
+ if (!xpon || mode >= __XPON_MODE_MAX)
+ return -EINVAL;
+ if (!(xpon->modes & XPON_MODE_CAP(mode)))
+ return -EOPNOTSUPP;
+
+ mutex_lock(&xpon->mode_lock);
+ if (xpon->mode_changing) {
+ ret = -EBUSY;
+ goto out;
+ }
+ if (netif_running(xpon->netdev)) {
+ ret = -EBUSY;
+ goto out;
+ }
+
+ old_mode = xpon->state.mode;
+ if (old_mode == mode)
+ goto out;
+
+ xpon->mode_changing = true;
+ xpon_device_report_carrier(xpon, false);
+ xpon_device_report_registration(xpon, XPON_REGISTRATION_DOWN);
+
+ if (xpon->ops && xpon->ops->set_mode) {
+ ret = xpon->ops->set_mode(xpon, mode);
+ if (ret)
+ goto out_changing;
+ }
+
+ WRITE_ONCE(xpon->state.mode, mode);
+ xpon_queue_event(xpon, XPON_EVENT_MODE);
+
+out_changing:
+ xpon->mode_changing = false;
+out:
+ mutex_unlock(&xpon->mode_lock);
+ return ret;
+}
+EXPORT_SYMBOL_GPL(xpon_device_set_mode);
+
+void xpon_device_report_registration(struct xpon_device *xpon,
+     enum xpon_registration_state state)
+{
+ unsigned long flags;
+
+ if (!xpon)
+ return;
+
+ spin_lock_irqsave(&xpon->state_lock, flags);
+ if (xpon->state.registration == state) {
+ spin_unlock_irqrestore(&xpon->state_lock, flags);
+ return;
+ }
+ xpon->state.registration = state;
+ xpon->pending_events |= XPON_EVENT_REGISTRATION;
+ spin_unlock_irqrestore(&xpon->state_lock, flags);
+ schedule_work(&xpon->notify_work);
+}
+EXPORT_SYMBOL_GPL(xpon_device_report_registration);
+
+void xpon_device_report_carrier(struct xpon_device *xpon, bool carrier)
+{
+ unsigned long flags;
+
+ if (!xpon)
+ return;
+
+ spin_lock_irqsave(&xpon->state_lock, flags);
+ if (xpon->state.carrier == carrier) {
+ spin_unlock_irqrestore(&xpon->state_lock, flags);
+ return;
+ }
+ xpon->state.carrier = carrier;
+ xpon->pending_events |= XPON_EVENT_CARRIER;
+ spin_unlock_irqrestore(&xpon->state_lock, flags);
+ schedule_work(&xpon->notify_work);
+}
+EXPORT_SYMBOL_GPL(xpon_device_report_carrier);
+
+void xpon_device_report_optical(struct xpon_device *xpon,
+ bool signal_detect, bool los)
+{
+ unsigned long flags;
+ unsigned long changed = 0;
+
+ if (!xpon)
+ return;
+
+ spin_lock_irqsave(&xpon->state_lock, flags);
+ if (!(xpon->state.valid & XPON_STATE_F_SIGNAL) ||
+    xpon->state.signal_detect != signal_detect)
+ changed |= XPON_EVENT_SIGNAL;
+ if (!(xpon->state.valid & XPON_STATE_F_LOS) || xpon->state.los != los)
+ changed |= XPON_EVENT_LOS;
+ xpon->state.valid |= XPON_STATE_F_SIGNAL | XPON_STATE_F_LOS;
+ xpon->state.signal_detect = signal_detect;
+ xpon->state.los = los;
+ xpon->pending_events |= changed;
+ spin_unlock_irqrestore(&xpon->state_lock, flags);
+
+ if (changed)
+ schedule_work(&xpon->notify_work);
+}
+EXPORT_SYMBOL_GPL(xpon_device_report_optical);
+
+void xpon_device_report_event(struct xpon_device *xpon, unsigned long event)
+{
+ xpon_queue_event(xpon, event);
+}
+EXPORT_SYMBOL_GPL(xpon_device_report_event);
+
+int xpon_device_register_notifier(struct xpon_device *xpon,
+  struct notifier_block *nb)
+{
+ if (!xpon || !nb)
+ return -EINVAL;
+ return blocking_notifier_chain_register(&xpon->notifier, nb);
+}
+EXPORT_SYMBOL_GPL(xpon_device_register_notifier);
+
+int xpon_device_unregister_notifier(struct xpon_device *xpon,
+    struct notifier_block *nb)
+{
+ if (!xpon || !nb)
+ return -EINVAL;
+ return blocking_notifier_chain_unregister(&xpon->notifier, nb);
+}
+EXPORT_SYMBOL_GPL(xpon_device_unregister_notifier);
+
+struct xpon_device *xpon_device_find_by_ifindex(u32 ifindex)
+{
+ struct xpon_device *xpon;
+
+ mutex_lock(&xpon_devices_lock);
+ list_for_each_entry(xpon, &xpon_devices, list) {
+ if (xpon->netdev->ifindex == ifindex) {
+ get_device(xpon->class_dev);
+ mutex_unlock(&xpon_devices_lock);
+ return xpon;
+ }
+ }
+ mutex_unlock(&xpon_devices_lock);
+ return NULL;
+}
+
+void xpon_device_put(struct xpon_device *xpon)
+{
+ if (xpon && xpon->class_dev)
+ put_device(xpon->class_dev);
+}
+
+static int __init xpon_init(void)
+{
+ int ret;
+
+ ret = xpon_sysfs_init();
+ if (ret)
+ return ret;
+
+ ret = xpon_genl_init();
+ if (ret) {
+ xpon_sysfs_exit();
+ return ret;
+ }
+
+ return 0;
+}
+
+static void __exit xpon_exit(void)
+{
+ xpon_genl_exit();
+ xpon_sysfs_exit();
+}
+
+module_init(xpon_init);
+module_exit(xpon_exit);
+
+MODULE_DESCRIPTION("Generic xPON subsystem");
+MODULE_LICENSE("GPL");
diff --git a/net/xpon/genl.c b/net/xpon/genl.c
new file mode 100644
index 000000000000..c6c8c0fd7ae6
--- /dev/null
+++ b/net/xpon/genl.c
@@ -0,0 +1,153 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <linux/module.h>
+#include <linux/netdevice.h>
+#include <net/genetlink.h>
+#include <net/xpon.h>
+
+#include "internal.h"
+
+static const struct nla_policy xpon_genl_policy[XPON_ATTR_MAX + 1] = {
+ [XPON_ATTR_IFINDEX] = { .type = NLA_U32 },
+ [XPON_ATTR_MODE] = { .type = NLA_U8 },
+};
+
+static struct genl_family xpon_genl_family;
+
+static int xpon_genl_put_state(struct sk_buff *msg, struct xpon_device *xpon)
+{
+ if (nla_put_u32(msg, XPON_ATTR_IFINDEX, xpon->netdev->ifindex) ||
+    nla_put_u8(msg, XPON_ATTR_MODE, xpon->state.mode) ||
+    nla_put_u32(msg, XPON_ATTR_AVAILABLE_MODES, xpon->modes) ||
+    nla_put_u8(msg, XPON_ATTR_REGISTRATION, xpon->state.registration) ||
+    nla_put_u8(msg, XPON_ATTR_CARRIER, xpon->state.carrier) ||
+    nla_put_u8(msg, XPON_ATTR_SIGNAL_DETECT, xpon->state.signal_detect) ||
+    nla_put_u8(msg, XPON_ATTR_LOS, xpon->state.los))
+ return -EMSGSIZE;
+
+ return 0;
+}
+
+static int xpon_genl_get(struct sk_buff *skb, struct genl_info *info)
+{
+ struct xpon_device *xpon;
+ struct sk_buff *msg;
+ void *hdr;
+ int ret;
+
+ if (!info->attrs[XPON_ATTR_IFINDEX])
+ return -EINVAL;
+
+ xpon = xpon_device_find_by_ifindex(
+ nla_get_u32(info->attrs[XPON_ATTR_IFINDEX]));
+ if (!xpon)
+ return -ENODEV;
+
+ msg = genlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ hdr = genlmsg_put_reply(msg, info, &xpon_genl_family, 0, XPON_CMD_GET);
+ if (!hdr) {
+ nlmsg_free(msg);
+ ret = -EMSGSIZE;
+ goto out;
+ }
+
+ ret = xpon_genl_put_state(msg, xpon);
+ if (ret) {
+ nlmsg_free(msg);
+ goto out;
+ }
+ genlmsg_end(msg, hdr);
+ ret = genlmsg_reply(msg, info);
+out:
+ xpon_device_put(xpon);
+ return ret;
+}
+
+static int xpon_genl_set_mode(struct sk_buff *skb, struct genl_info *info)
+{
+ struct xpon_device *xpon;
+ u8 mode;
+ int ret;
+
+ if (!info->attrs[XPON_ATTR_IFINDEX] || !info->attrs[XPON_ATTR_MODE])
+ return -EINVAL;
+ mode = nla_get_u8(info->attrs[XPON_ATTR_MODE]);
+
+ xpon = xpon_device_find_by_ifindex(
+ nla_get_u32(info->attrs[XPON_ATTR_IFINDEX]));
+ if (!xpon)
+ return -ENODEV;
+ ret = xpon_device_set_mode(xpon, mode);
+ xpon_device_put(xpon);
+ return ret;
+}
+
+static const struct genl_ops xpon_genl_ops[] = {
+ {
+ .cmd = XPON_CMD_GET,
+ .policy = xpon_genl_policy,
+ .doit = xpon_genl_get,
+ },
+ {
+ .cmd = XPON_CMD_GET_STATE,
+ .policy = xpon_genl_policy,
+ .doit = xpon_genl_get,
+ },
+ {
+ .cmd = XPON_CMD_SET_MODE,
+ .flags = GENL_ADMIN_PERM,
+ .policy = xpon_genl_policy,
+ .doit = xpon_genl_set_mode,
+ },
+};
+
+static const struct genl_multicast_group xpon_mcgrps[] = {
+ { .name = "events" },
+};
+
+static struct genl_family xpon_genl_family = {
+ .name = XPON_GENL_NAME,
+ .version = XPON_GENL_VERSION,
+ .maxattr = XPON_ATTR_MAX,
+ .module = THIS_MODULE,
+ .ops = xpon_genl_ops,
+ .n_ops = ARRAY_SIZE(xpon_genl_ops),
+ .mcgrps = xpon_mcgrps,
+ .n_mcgrps = ARRAY_SIZE(xpon_mcgrps),
+};
+
+void xpon_genl_notify(struct xpon_device *xpon, unsigned long changed)
+{
+ struct sk_buff *msg;
+ void *hdr;
+
+ msg = genlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg)
+ return;
+ hdr = genlmsg_put(msg, 0, 0, &xpon_genl_family, 0, XPON_CMD_EVENT);
+ if (!hdr)
+ goto free;
+ if (xpon_genl_put_state(msg, xpon) ||
+    nla_put_u32(msg, XPON_ATTR_CHANGED, changed))
+ goto free;
+ genlmsg_end(msg, hdr);
+ genlmsg_multicast(&xpon_genl_family, msg, 0, 0, GFP_KERNEL);
+ return;
+free:
+ nlmsg_free(msg);
+}
+
+int xpon_genl_init(void)
+{
+ return genl_register_family(&xpon_genl_family);
+}
+
+void xpon_genl_exit(void)
+{
+ genl_unregister_family(&xpon_genl_family);
+}
diff --git a/net/xpon/internal.h b/net/xpon/internal.h
new file mode 100644
index 000000000000..8ca207f48d16
--- /dev/null
+++ b/net/xpon/internal.h
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _NET_XPON_INTERNAL_H
+#define _NET_XPON_INTERNAL_H
+
+#include <linux/device.h>
+#include <linux/list.h>
+#include <linux/mutex.h>
+#include <linux/notifier.h>
+#include <linux/spinlock.h>
+#include <linux/workqueue.h>
+#include <net/xpon.h>
+
+struct xpon_device {
+ struct list_head list;
+ struct device *parent;
+ struct device *class_dev;
+ struct net_device *netdev;
+ struct device *optical;
+ const struct xpon_device_ops *ops;
+ void *priv;
+
+ unsigned long modes;
+ struct xpon_state state;
+ spinlock_t state_lock;
+ struct mutex mode_lock;
+ bool mode_changing;
+
+ unsigned long pending_events;
+ struct xpon_state notify_old;
+ struct work_struct notify_work;
+ struct blocking_notifier_head notifier;
+
+ struct led_classdev *pon_led;
+ struct led_classdev *los_led;
+ struct led_classdev *fiber_led;
+
+ void *omci;
+ void *oam;
+};
+
+extern struct class *xpon_class;
+
+int xpon_sysfs_init(void);
+void xpon_sysfs_exit(void);
+int xpon_sysfs_register(struct xpon_device *xpon);
+void xpon_sysfs_unregister(struct xpon_device *xpon);
+void xpon_sysfs_notify(struct xpon_device *xpon, unsigned long changed);
+
+void xpon_leds_update(struct xpon_device *xpon,
+      const struct xpon_state *state);
+
+int xpon_genl_init(void);
+void xpon_genl_exit(void);
+void xpon_genl_notify(struct xpon_device *xpon, unsigned long changed);
+
+struct xpon_device *xpon_device_find_by_ifindex(u32 ifindex);
+void xpon_device_put(struct xpon_device *xpon);
+
+#endif /* _NET_XPON_INTERNAL_H */
diff --git a/net/xpon/leds.c b/net/xpon/leds.c
new file mode 100644
index 000000000000..57059e108690
--- /dev/null
+++ b/net/xpon/leds.c
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <linux/leds.h>
+
+#include "internal.h"
+
+static void xpon_led_set(struct led_classdev *led, enum led_brightness value)
+{
+ if (led)
+ led_set_brightness(led, value);
+}
+
+void xpon_leds_update(struct xpon_device *xpon,
+      const struct xpon_state *state)
+{
+ unsigned long delay_on = 250;
+ unsigned long delay_off = 250;
+
+ if ((state->valid & XPON_STATE_F_LOS) && state->los) {
+ xpon_led_set(xpon->fiber_led, LED_OFF);
+ xpon_led_set(xpon->los_led, LED_FULL);
+ xpon_led_set(xpon->pon_led, LED_OFF);
+ return;
+ }
+
+ if (state->valid & XPON_STATE_F_SIGNAL)
+ xpon_led_set(xpon->fiber_led,
+     state->signal_detect ? LED_FULL : LED_OFF);
+ xpon_led_set(xpon->los_led, LED_OFF);
+
+ switch (state->registration) {
+ case XPON_REGISTRATION_OPERATIONAL:
+ xpon_led_set(xpon->pon_led, LED_FULL);
+ break;
+ case XPON_REGISTRATION_DISCOVERY:
+ case XPON_REGISTRATION_REGISTERING:
+ if (xpon->pon_led)
+ led_blink_set(xpon->pon_led, &delay_on, &delay_off);
+ break;
+ default:
+ xpon_led_set(xpon->pon_led, LED_OFF);
+ break;
+ }
+}
diff --git a/net/xpon/oam/Makefile b/net/xpon/oam/Makefile
new file mode 100644
index 000000000000..09d83f948771
--- /dev/null
+++ b/net/xpon/oam/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_XPON_OAM) += xpon-oam.o
+xpon-oam-y := core.o wire.o sysfs.o
diff --git a/net/xpon/oam/core.c b/net/xpon/oam/core.c
new file mode 100644
index 000000000000..9d8d4c5fe473
--- /dev/null
+++ b/net/xpon/oam/core.c
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <linux/err.h>
+#include <linux/slab.h>
+#include <linux/skbuff.h>
+#include <net/xpon.h>
+#include <net/xpon/oam.h>
+
+#include "../internal.h"
+#include "internal.h"
+
+struct xpon_oam *xpon_oam_register(struct xpon_device *xpon,
+   const struct xpon_oam_ops *ops,
+   void *priv)
+{
+ struct xpon_oam *oam;
+ int ret;
+
+ if (!xpon || !ops || !ops->xmit)
+ return ERR_PTR(-EINVAL);
+ if (xpon->oam)
+ return ERR_PTR(-EBUSY);
+
+ oam = kzalloc(sizeof(*oam), GFP_KERNEL);
+ if (!oam)
+ return ERR_PTR(-ENOMEM);
+
+ mutex_init(&oam->lock);
+ oam->xpon = xpon;
+ oam->ops = ops;
+ oam->priv = priv;
+
+ ret = xpon_oam_sysfs_register(oam);
+ if (ret) {
+ kfree(oam);
+ return ERR_PTR(ret);
+ }
+
+ xpon->oam = oam;
+ xpon_device_report_event(xpon, XPON_EVENT_OAM);
+ return oam;
+}
+EXPORT_SYMBOL_GPL(xpon_oam_register);
+
+void xpon_oam_unregister(struct xpon_oam *oam)
+{
+ if (!oam)
+ return;
+
+ if (oam->xpon && oam->xpon->oam == oam)
+ oam->xpon->oam = NULL;
+ xpon_oam_sysfs_unregister(oam);
+ kfree(oam);
+}
+EXPORT_SYMBOL_GPL(xpon_oam_unregister);
+
+void *xpon_oam_priv(struct xpon_oam *oam)
+{
+ return oam ? oam->priv : NULL;
+}
+EXPORT_SYMBOL_GPL(xpon_oam_priv);
+
+void xpon_oam_llid_registered(struct xpon_oam *oam, u8 index, u16 llid)
+{
+ if (!oam || index >= XPON_OAM_MAX_LLID)
+ return;
+
+ mutex_lock(&oam->lock);
+ oam->llids[index].llid = llid;
+ oam->llids[index].registered = true;
+ memset(&oam->llids[index].remote, 0,
+       sizeof(oam->llids[index].remote));
+ mutex_unlock(&oam->lock);
+ xpon_device_report_event(oam->xpon, XPON_EVENT_LLID | XPON_EVENT_OAM);
+}
+EXPORT_SYMBOL_GPL(xpon_oam_llid_registered);
+
+void xpon_oam_llid_unregistered(struct xpon_oam *oam, u8 index)
+{
+ if (!oam || index >= XPON_OAM_MAX_LLID)
+ return;
+
+ mutex_lock(&oam->lock);
+ memset(&oam->llids[index], 0, sizeof(oam->llids[index]));
+ mutex_unlock(&oam->lock);
+ xpon_device_report_event(oam->xpon, XPON_EVENT_LLID | XPON_EVENT_OAM);
+}
+EXPORT_SYMBOL_GPL(xpon_oam_llid_unregistered);
+
+void xpon_oam_receive(struct xpon_oam *oam, struct sk_buff *skb,
+      u8 llid_index)
+{
+ if (!oam || !skb || llid_index >= XPON_OAM_MAX_LLID) {
+ kfree_skb(skb);
+ return;
+ }
+
+ if (!oam->llids[llid_index].registered) {
+ kfree_skb(skb);
+ return;
+ }
+
+ oam->llids[llid_index].rx_oampdu++;
+ if (xpon_oam_wire_receive(oam, skb, llid_index))
+ kfree_skb(skb);
+}
+EXPORT_SYMBOL_GPL(xpon_oam_receive);
diff --git a/net/xpon/oam/internal.h b/net/xpon/oam/internal.h
new file mode 100644
index 000000000000..5b3d21ce25c2
--- /dev/null
+++ b/net/xpon/oam/internal.h
@@ -0,0 +1,58 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _NET_XPON_OAM_INTERNAL_H
+#define _NET_XPON_OAM_INTERNAL_H
+
+#include <linux/mutex.h>
+#include <linux/types.h>
+#include <net/xpon/oam.h>
+
+#define XPON_OAM_SUBTYPE 0x03
+
+enum xpon_oam_code {
+ XPON_OAM_CODE_INFO = 0x00,
+ XPON_OAM_CODE_EVENT = 0x01,
+ XPON_OAM_CODE_VAR_REQ = 0x02,
+ XPON_OAM_CODE_VAR_RESP = 0x03,
+ XPON_OAM_CODE_LOOPBACK = 0x04,
+ XPON_OAM_CODE_ORG_SPECIFIC = 0xfe,
+};
+
+struct xpon_oam_hdr {
+ u8 subtype;
+ __be16 flags;
+ u8 code;
+} __packed;
+
+struct xpon_oam_info {
+ u8 version;
+ u16 revision;
+ u8 state;
+ u8 config;
+ u16 pdu_config;
+ u8 oui[3];
+ u32 vendor_info;
+ bool valid;
+};
+
+struct xpon_oam_llid {
+ u16 llid;
+ bool registered;
+ struct xpon_oam_info remote;
+ u64 rx_oampdu;
+ u64 tx_oampdu;
+};
+
+struct xpon_oam {
+ struct xpon_device *xpon;
+ const struct xpon_oam_ops *ops;
+ void *priv;
+ struct mutex lock;
+ struct xpon_oam_llid llids[XPON_OAM_MAX_LLID];
+};
+
+int xpon_oam_wire_receive(struct xpon_oam *oam, struct sk_buff *skb,
+  u8 llid_index);
+int xpon_oam_sysfs_register(struct xpon_oam *oam);
+void xpon_oam_sysfs_unregister(struct xpon_oam *oam);
+
+#endif /* _NET_XPON_OAM_INTERNAL_H */
diff --git a/net/xpon/oam/sysfs.c b/net/xpon/oam/sysfs.c
new file mode 100644
index 000000000000..05cef1c3d4ee
--- /dev/null
+++ b/net/xpon/oam/sysfs.c
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <linux/device.h>
+#include <linux/sysfs.h>
+#include <net/xpon.h>
+
+#include "../internal.h"
+#include "internal.h"
+
+static ssize_t enabled_show(struct device *dev, struct device_attribute *attr,
+    char *buf)
+{
+ struct xpon_device *xpon = dev_get_drvdata(dev);
+
+ return sysfs_emit(buf, "%u\n", !!xpon->oam);
+}
+static DEVICE_ATTR_RO(enabled);
+
+static struct attribute *xpon_oam_attrs[] = {
+ &dev_attr_enabled.attr,
+ NULL,
+};
+
+static const struct attribute_group xpon_oam_group = {
+ .name = "oam",
+ .attrs = xpon_oam_attrs,
+};
+
+int xpon_oam_sysfs_register(struct xpon_oam *oam)
+{
+ return sysfs_create_group(&oam->xpon->class_dev->kobj, &xpon_oam_group);
+}
+
+void xpon_oam_sysfs_unregister(struct xpon_oam *oam)
+{
+ if (oam->xpon && oam->xpon->class_dev)
+ sysfs_remove_group(&oam->xpon->class_dev->kobj, &xpon_oam_group);
+}
diff --git a/net/xpon/oam/wire.c b/net/xpon/oam/wire.c
new file mode 100644
index 000000000000..ddfb66d44928
--- /dev/null
+++ b/net/xpon/oam/wire.c
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/* IEEE 802.3ah OAMPDU wire handling. */
+
+#include <linux/etherdevice.h>
+#include <linux/skbuff.h>
+#include <linux/unaligned.h>
+
+#include "internal.h"
+
+#define XPON_OAM_INFO_TLV 0x01
+#define XPON_OAM_INFO_TLV_LEN 16
+#define XPON_OAM_END_TLV 0x00
+
+static int xpon_oam_parse_information(struct xpon_oam *oam,
+      struct sk_buff *skb, u8 llid_index)
+{
+ struct xpon_oam_info remote = {};
+ const u8 *p;
+
+ if (skb->len < sizeof(struct xpon_oam_hdr) + 2)
+ return -EINVAL;
+
+ p = skb->data + sizeof(struct xpon_oam_hdr);
+ while (p + 2 <= skb_tail_pointer(skb)) {
+ u8 type = p[0];
+ u8 len = p[1];
+
+ if (type == XPON_OAM_END_TLV)
+ break;
+ if (len < 2 || p + len > skb_tail_pointer(skb))
+ return -EINVAL;
+ if (type == XPON_OAM_INFO_TLV && len >= XPON_OAM_INFO_TLV_LEN) {
+ remote.version = p[2];
+ remote.revision = get_unaligned_be16(p + 3);
+ remote.state = p[5];
+ remote.config = p[6];
+ remote.pdu_config = get_unaligned_be16(p + 7);
+ memcpy(remote.oui, p + 9, sizeof(remote.oui));
+ remote.vendor_info = get_unaligned_be32(p + 12);
+ remote.valid = true;
+ }
+ p += len;
+ }
+
+ if (remote.valid) {
+ mutex_lock(&oam->lock);
+ oam->llids[llid_index].remote = remote;
+ mutex_unlock(&oam->lock);
+ }
+ return 0;
+}
+
+int xpon_oam_wire_receive(struct xpon_oam *oam, struct sk_buff *skb,
+  u8 llid_index)
+{
+ struct xpon_oam_hdr *hdr;
+
+ if (!pskb_may_pull(skb, sizeof(*hdr)))
+ return -EINVAL;
+ hdr = (struct xpon_oam_hdr *)skb->data;
+ if (hdr->subtype != XPON_OAM_SUBTYPE)
+ return -EPROTO;
+
+ switch (hdr->code) {
+ case XPON_OAM_CODE_INFO:
+ return xpon_oam_parse_information(oam, skb, llid_index);
+ case XPON_OAM_CODE_EVENT:
+ case XPON_OAM_CODE_VAR_REQ:
+ case XPON_OAM_CODE_VAR_RESP:
+ case XPON_OAM_CODE_LOOPBACK:
+ case XPON_OAM_CODE_ORG_SPECIFIC:
+ return -EOPNOTSUPP;
+ default:
+ return -EPROTO;
+ }
+}
diff --git a/net/xpon/omci/Kconfig b/net/xpon/omci/Kconfig
new file mode 100644
index 000000000000..4a3a9609aedf
--- /dev/null
+++ b/net/xpon/omci/Kconfig
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+# OMCI configuration is exposed by net/xpon/Kconfig.
diff --git a/net/xpon/omci/Makefile b/net/xpon/omci/Makefile
new file mode 100644
index 000000000000..5fe45f7ad04a
--- /dev/null
+++ b/net/xpon/omci/Makefile
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_XPON_OMCI) += omci.o
+omci-y := core.o agent.o me.o wire.o identity.o profiles.o sysfs.o
diff --git a/net/xpon/omci/agent.c b/net/xpon/omci/agent.c
new file mode 100644
index 000000000000..01e2691ba622
--- /dev/null
+++ b/net/xpon/omci/agent.c
@@ -0,0 +1,5036 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * In-kernel ONU Management and Control Interface agent
+ *
+ * The agent implements the baseline OMCI transaction path and owns the
+ * operational MIB. Managed entities without a dedicated hardware callback
+ * are stored as opaque baseline attribute data so that userspace can inspect,
+ * persist and amend them through Generic Netlink.
+ */
+
+#include <linux/bitmap.h>
+#include <linux/errno.h>
+#include <linux/int_log.h>
+#include <linux/jhash.h>
+#include <linux/jiffies.h>
+#include <linux/if_vlan.h>
+#include <linux/kernel.h>
+#include <linux/math.h>
+#include <linux/math64.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/unaligned.h>
+#include <net/netlink.h>
+
+#include "internal.h"
+#include "me.h"
+#include "wire.h"
+
+#define OMCI_MSG_TYPE_CREATE 4
+#define OMCI_MSG_TYPE_DELETE 6
+#define OMCI_MSG_TYPE_SET 8
+#define OMCI_MSG_TYPE_GET 9
+#define OMCI_MSG_TYPE_GET_ALL_ALARMS 11
+#define OMCI_MSG_TYPE_GET_ALL_ALARMS_NEXT 12
+#define OMCI_MSG_TYPE_MIB_UPLOAD 13
+#define OMCI_MSG_TYPE_MIB_UPLOAD_NEXT 14
+#define OMCI_MSG_TYPE_MIB_RESET 15
+#define OMCI_MSG_TYPE_ALARM_NOTIFICATION 16
+#define OMCI_MSG_TYPE_TEST 18
+#define OMCI_MSG_TYPE_START_SW_DOWNLOAD 19
+#define OMCI_MSG_TYPE_DOWNLOAD_SECTION 20
+#define OMCI_MSG_TYPE_END_SW_DOWNLOAD 21
+#define OMCI_MSG_TYPE_ACTIVATE_SW 22
+#define OMCI_MSG_TYPE_COMMIT_SW 23
+#define OMCI_MSG_TYPE_SYNC_TIME 24
+#define OMCI_MSG_TYPE_REBOOT 25
+#define OMCI_MSG_TYPE_GET_NEXT 26
+#define OMCI_MSG_TYPE_TEST_RESULT 27
+#define OMCI_MSG_TYPE_GET_CURRENT_DATA 28
+#define OMCI_MSG_TYPE_SET_TABLE 29
+
+#define OMCI_RESULT_SUCCESS 0
+#define OMCI_RESULT_PROCESSING_ERROR 1
+#define OMCI_RESULT_NOT_SUPPORTED 2
+#define OMCI_RESULT_PARAMETER_ERROR 3
+#define OMCI_RESULT_UNKNOWN_ME 4
+#define OMCI_RESULT_UNKNOWN_INSTANCE 5
+#define OMCI_RESULT_DEVICE_BUSY 6
+#define OMCI_RESULT_INSTANCE_EXISTS 7
+#define OMCI_RESULT_ATTRIBUTE_FAILED 9
+
+#define OMCI_BRIDGE_TP_PPTP_ETH_UNI 1
+#define OMCI_BRIDGE_TP_8021P_MAPPER 3
+#define OMCI_BRIDGE_TP_GEM_IWTP 5
+#define OMCI_BRIDGE_TP_MULTICAST_GEM_IWTP 6
+#define OMCI_BRIDGE_TP_VEIP 11
+
+static void omci_agent_reset_table_snapshot_locked(struct omci_agent *agent);
+
+static bool omci_agent_result_can_be_faked(u8 result)
+{
+ switch (result) {
+ case OMCI_RESULT_NOT_SUPPORTED:
+ case OMCI_RESULT_UNKNOWN_ME:
+ case OMCI_RESULT_UNKNOWN_INSTANCE:
+ case OMCI_RESULT_INSTANCE_EXISTS:
+ return true;
+ default:
+ return false;
+ }
+}
+
+#define OMCI_PRIORITY_QUEUE_ATTR_MASK 0xfff0
+#define OMCI_TRAFFIC_SCHEDULER_ATTR_MASK 0xf000
+
+#define OMCI_PRIORITY_QUEUE_QUEUE_CONFIG_MASK 0x8000
+#define OMCI_PRIORITY_QUEUE_MAX_SIZE_MASK 0x4000
+#define OMCI_PRIORITY_QUEUE_ALLOC_SIZE_MASK 0x2000
+#define OMCI_PRIORITY_QUEUE_DISCARD_RESET_MASK 0x1000
+#define OMCI_PRIORITY_QUEUE_DISCARD_THRESHOLD_MASK 0x0800
+#define OMCI_PRIORITY_QUEUE_RELATED_PORT_MASK 0x0400
+#define OMCI_PRIORITY_QUEUE_SCHEDULER_MASK 0x0200
+#define OMCI_PRIORITY_QUEUE_WEIGHT_MASK 0x0100
+#define OMCI_PRIORITY_QUEUE_BACKPRESSURE_OP_MASK 0x0080
+#define OMCI_PRIORITY_QUEUE_BACKPRESSURE_TIME_MASK 0x0040
+#define OMCI_PRIORITY_QUEUE_BP_OCCUR_MASK 0x0020
+#define OMCI_PRIORITY_QUEUE_BP_CLEAR_MASK 0x0010
+
+#define OMCI_TRAFFIC_SCHEDULER_TCONT_MASK 0x8000
+#define OMCI_TRAFFIC_SCHEDULER_PARENT_MASK 0x4000
+#define OMCI_TRAFFIC_SCHEDULER_POLICY_MASK 0x2000
+#define OMCI_TRAFFIC_SCHEDULER_PRIORITY_MASK 0x1000
+
+#define OMCI_DEFAULT_TCONT_COUNT 8
+#define OMCI_DEFAULT_QUEUES_PER_TCONT 1
+#define OMCI_MAX_TCONT_RESOURCES 32
+#define OMCI_MAX_QUEUES_PER_TCONT 8
+
+#define OMCI_OMCI_ME_TYPE_TABLE_MASK BIT(15)
+#define OMCI_OMCI_MESSAGE_TYPE_TABLE_MASK BIT(14)
+#define OMCI_TABLE_SNAPSHOT_TIMEOUT (60 * HZ)
+#define OMCI_BASELINE_TABLE_FRAGMENT_LEN 29
+
+#define OMCI_MANAGED_ENTITY_NAME_MASK BIT(15)
+#define OMCI_MANAGED_ENTITY_ATTR_TABLE_MASK BIT(14)
+#define OMCI_MANAGED_ENTITY_ACCESS_MASK BIT(13)
+#define OMCI_MANAGED_ENTITY_ALARMS_MASK BIT(12)
+#define OMCI_MANAGED_ENTITY_AVCS_MASK BIT(11)
+#define OMCI_MANAGED_ENTITY_ACTIONS_MASK BIT(10)
+#define OMCI_MANAGED_ENTITY_INSTANCES_MASK BIT(9)
+#define OMCI_MANAGED_ENTITY_SUPPORT_MASK BIT(8)
+#define OMCI_MANAGED_ENTITY_TABLE_MASKS \
+ (OMCI_MANAGED_ENTITY_ATTR_TABLE_MASK | \
+ OMCI_MANAGED_ENTITY_ALARMS_MASK | \
+ OMCI_MANAGED_ENTITY_AVCS_MASK | \
+ OMCI_MANAGED_ENTITY_INSTANCES_MASK)
+
+#define OMCI_ATTRIBUTE_NAME_MASK BIT(15)
+#define OMCI_ATTRIBUTE_SIZE_MASK BIT(14)
+#define OMCI_ATTRIBUTE_ACCESS_MASK BIT(13)
+#define OMCI_ATTRIBUTE_FORMAT_MASK BIT(12)
+#define OMCI_ATTRIBUTE_LOWER_LIMIT_MASK BIT(11)
+#define OMCI_ATTRIBUTE_UPPER_LIMIT_MASK BIT(10)
+#define OMCI_ATTRIBUTE_BIT_FIELD_MASK BIT(9)
+#define OMCI_ATTRIBUTE_CODE_POINTS_MASK BIT(8)
+#define OMCI_ATTRIBUTE_SUPPORT_MASK BIT(7)
+#define OMCI_ATTRIBUTE_TABLE_MASKS OMCI_ATTRIBUTE_CODE_POINTS_MASK
+
+#define OMCI_CAPABILITY_NAME_LEN 25
+#define OMCI_ATTRIBUTE_FORMAT_BIT_FIELD 2
+#define OMCI_ATTRIBUTE_FORMAT_UNSIGNED 4
+#define OMCI_ATTRIBUTE_FORMAT_STRING 5
+#define OMCI_ATTRIBUTE_FORMAT_TABLE 7
+
+#define OMCI_ONU_G_DYING_GASP_ALARM 7
+#define OMCI_ALARM_BITMAP_LEN 28
+#define OMCI_ALARM_SEQUENCE_OFFSET (8 + OMCI_ALARM_BITMAP_LEN)
+
+#define OMCI_ANI_G_ENTITY_ID 0x8001
+#define OMCI_ETHERNET_SLOT 0x01
+#define OMCI_VEIP_SLOT 0x0a
+#define OMCI_GPON_SLOT 0x80
+#define OMCI_ETHERNET_UNIT_TYPE 47
+#define OMCI_VEIP_UNIT_TYPE 48
+#define OMCI_GPON_UNIT_TYPE 248
+#define OMCI_EQUIPMENT_ENTITY_ID(_slot) (0x0100 | (_slot))
+#define OMCI_UNI_ENTITY_ID(_slot, _port) (((_slot) << 8) | (_port))
+#define OMCI_ANI_G_RX_LEVEL_MASK 0x0040
+#define OMCI_ANI_G_TX_LEVEL_MASK 0x0004
+#define OMCI_ANI_G_OPTICAL_LEVEL_MASK (OMCI_ANI_G_RX_LEVEL_MASK | \
+ OMCI_ANI_G_TX_LEVEL_MASK)
+
+#define OMCI_TEST_TYPE_UNSUPPORTED 0
+#define OMCI_TEST_TYPE_VOLTAGE 1
+#define OMCI_TEST_TYPE_RX_POWER 3
+#define OMCI_TEST_TYPE_TX_POWER 5
+#define OMCI_TEST_TYPE_BIAS 9
+#define OMCI_TEST_TYPE_TEMPERATURE 12
+
+#define OMCI_OPTICAL_LEVEL_SCALE 500
+#define OMCI_NW_TO_DBM_0P002_OFFSET 30000
+#define OMCI_NW_TO_DBUW_0P002_OFFSET 15000
+
+#define OMCI_VLAN_FILTER_LIST_MASK 0x8000
+#define OMCI_VLAN_FILTER_FORWARD_MASK 0x4000
+#define OMCI_VLAN_FILTER_COUNT_MASK 0x2000
+
+#define OMCI_EXT_VLAN_ASSOC_TYPE_MASK 0x8000
+#define OMCI_EXT_VLAN_MAX_SIZE_MASK 0x4000
+#define OMCI_EXT_VLAN_INPUT_TPID_MASK 0x2000
+#define OMCI_EXT_VLAN_OUTPUT_TPID_MASK 0x1000
+#define OMCI_EXT_VLAN_DOWNSTREAM_MASK 0x0800
+#define OMCI_EXT_VLAN_TABLE_MASK 0x0400
+#define OMCI_EXT_VLAN_ASSOC_PTR_MASK 0x0200
+#define OMCI_EXT_VLAN_DSCP_MAP_MASK 0x0100
+
+#define OMCI_OLT_G_VENDOR_ID_MASK 0x8000
+#define OMCI_OLT_G_EQUIPMENT_ID_MASK 0x4000
+#define OMCI_OLT_G_VERSION_MASK 0x2000
+#define OMCI_OMCC_VERSION_G988_2010_BASELINE 0xa0
+#define OMCI_OLT_G_TIME_OF_DAY_MASK 0x1000
+#define OMCI_OLT_G_TIME_OF_DAY_LEN 14
+
+#define OMCI_ONU_G_ATTR_MASK GENMASK(15, 3)
+#define OMCI_ONU2_G_ATTR_MASK GENMASK(15, 2)
+
+#define OMCI_ATTR_VALUE_ZERO U8_MAX
+
+struct omci_get_attr_layout {
+ u16 mask;
+ u8 offset;
+ u8 len;
+};
+
+static const struct omci_get_attr_layout omci_onu_g_attr_layout[] = {
+ { BIT(15), 0, 4 }, /* Vendor ID */
+ { BIT(14), 4, 14 }, /* Version */
+ { BIT(13), 18, 8 }, /* Serial number */
+ { BIT(12), 26, 1 }, /* Traffic management option */
+ { BIT(11), 27, 1 }, /* Deprecated */
+ { BIT(10), 28, 1 }, /* Battery backup */
+ { BIT(9), 29, 1 }, /* Administrative state */
+ { BIT(8), 30, 1 }, /* Operational state */
+ { BIT(7), 31, 1 }, /* ONU survival time */
+ { BIT(6), OMCI_ATTR_VALUE_ZERO, 24 }, /* Logical ONU ID */
+ { BIT(5), OMCI_ATTR_VALUE_ZERO, 12 }, /* Logical password */
+ { BIT(4), OMCI_ATTR_VALUE_ZERO, 1 }, /* Credentials status */
+ { BIT(3), OMCI_ATTR_VALUE_ZERO, 1 }, /* Extended TC layer */
+};
+
+static const struct omci_get_attr_layout omci_onu2_g_attr_layout[] = {
+ { BIT(15), 0, 20 }, /* Equipment ID */
+ { BIT(14), 20, 1 }, /* OMCC version */
+ { BIT(13), 21, 2 }, /* Vendor product code */
+ { BIT(12), 23, 1 }, /* Security capability */
+ { BIT(11), 24, 1 }, /* Security mode */
+ { BIT(10), 25, 2 }, /* Total priority queues */
+ { BIT(9), 27, 1 }, /* Total traffic schedulers */
+ { BIT(8), 28, 1 }, /* Deprecated */
+ { BIT(7), 29, 2 }, /* Total GEM port IDs */
+ { BIT(6), OMCI_ATTR_VALUE_ZERO, 4 }, /* System uptime */
+ { BIT(5), OMCI_ATTR_VALUE_ZERO, 2 }, /* Connectivity capability */
+ { BIT(4), 31, 1 }, /* Connectivity mode */
+ { BIT(3), OMCI_ATTR_VALUE_ZERO, 2 }, /* QoS configuration */
+ { BIT(2), OMCI_ATTR_VALUE_ZERO, 2 }, /* Priority queue scale */
+};
+
+static const u8 *omci_olt_g_consume(const u8 **data, size_t *len,
+    size_t size)
+{
+ const u8 *value;
+
+ if (*len < size)
+ return NULL;
+ value = *data;
+ *data += size;
+ *len -= size;
+
+ return value;
+}
+
+static void omci_olt_g_copy_string(char *dest, size_t dest_len,
+   const u8 *source, size_t source_len)
+{
+ size_t len = min(source_len, dest_len - 1);
+
+ memcpy(dest, source, len);
+ dest[len] = '\0';
+}
+
+static int omci_olt_g_parse_set(struct omci_mib_object *object, u16 mask,
+ const u8 *data, size_t len)
+{
+ struct omci_olt_g *olt = &object->olt_g;
+ const u8 *value;
+
+ if (mask & OMCI_OLT_G_VENDOR_ID_MASK) {
+ value = omci_olt_g_consume(&data, &len,
+   OMCI_OLT_VENDOR_ID_LEN);
+ if (!value)
+ return -EINVAL;
+ omci_olt_g_copy_string(olt->vendor_id, sizeof(olt->vendor_id),
+       value, OMCI_OLT_VENDOR_ID_LEN);
+ olt->vendor_id_valid = true;
+ }
+ if (mask & OMCI_OLT_G_EQUIPMENT_ID_MASK) {
+ value = omci_olt_g_consume(&data, &len,
+   OMCI_OLT_EQUIPMENT_ID_LEN);
+ if (!value)
+ return -EINVAL;
+ omci_olt_g_copy_string(olt->equipment_id,
+       sizeof(olt->equipment_id), value,
+       OMCI_OLT_EQUIPMENT_ID_LEN);
+ olt->equipment_id_valid = true;
+ }
+ if (mask & OMCI_OLT_G_VERSION_MASK) {
+ value = omci_olt_g_consume(&data, &len, OMCI_OLT_VERSION_LEN);
+ if (!value)
+ return -EINVAL;
+ omci_olt_g_copy_string(olt->version, sizeof(olt->version),
+       value, OMCI_OLT_VERSION_LEN);
+ olt->version_valid = true;
+ }
+ if (mask & OMCI_OLT_G_TIME_OF_DAY_MASK) {
+ value = omci_olt_g_consume(&data, &len,
+   OMCI_OLT_G_TIME_OF_DAY_LEN);
+ if (!value)
+ return -EINVAL;
+ }
+ olt->valid = olt->vendor_id_valid || olt->equipment_id_valid ||
+     olt->version_valid;
+
+ return 0;
+}
+
+static void omci_vlan_filter_parse_entries(struct
omci_vlan_tagging_filter *filter,
+   const u8 *data)
+{
+ unsigned int i;
+
+ for (i = 0; i < OMCI_VLAN_FILTER_MAX_ENTRIES; i++) {
+ u16 tci = get_unaligned_be16(data + i * sizeof(tci));
+
+ filter->entries[i].tci = tci;
+ filter->entries[i].vid = tci & GENMASK(11, 0);
+ filter->entries[i].dei = (tci >> 12) & 0x1;
+ filter->entries[i].pbit = (tci >> 13) & 0x7;
+ }
+}
+
+static void omci_vlan_filter_parse_create(struct omci_mib_object *object,
+  const u8 *content)
+{
+ struct omci_vlan_tagging_filter *filter = &object->vlan_filter;
+
+ omci_vlan_filter_parse_entries(filter, content);
+ filter->forward_operation = content[24];
+ filter->num_entries = min_t(u8, content[25],
+    OMCI_VLAN_FILTER_MAX_ENTRIES);
+ filter->valid = true;
+}
+
+static int omci_vlan_filter_parse_set(struct omci_mib_object *object,
+      u16 mask, const u8 *data,
+      size_t len)
+{
+ struct omci_vlan_tagging_filter *filter = &object->vlan_filter;
+
+ if (mask & OMCI_VLAN_FILTER_LIST_MASK) {
+ if (len < 24)
+ return -EINVAL;
+ omci_vlan_filter_parse_entries(filter, data);
+ data += 24;
+ len -= 24;
+ }
+ if (mask & OMCI_VLAN_FILTER_FORWARD_MASK) {
+ if (!len)
+ return -EINVAL;
+ filter->forward_operation = *data++;
+ len--;
+ }
+ if (mask & OMCI_VLAN_FILTER_COUNT_MASK) {
+ if (!len)
+ return -EINVAL;
+ filter->num_entries = min_t(u8, *data,
+    OMCI_VLAN_FILTER_MAX_ENTRIES);
+ }
+ filter->valid = true;
+
+ return 0;
+}
+
+static bool omci_ext_vlan_rule_is_delete(const u8 *raw)
+{
+ unsigned int i;
+
+ for (i = 8; i < OMCI_EXT_VLAN_RULE_LEN; i++)
+ if (raw[i] != 0xff)
+ return false;
+
+ return true;
+}
+
+static void omci_ext_vlan_parse_rule(struct omci_extended_vlan_rule *rule,
+     const u8 *raw)
+{
+ u32 word;
+
+ memcpy(rule->raw, raw, sizeof(rule->raw));
+ word = get_unaligned_be32(raw);
+ rule->filter_outer_pbit = (word >> 28) & 0xf;
+ rule->filter_outer_vid = (word >> 15) & 0x1fff;
+ rule->filter_outer_tpid_dei = (word >> 12) & 0x7;
+
+ word = get_unaligned_be32(raw + 4);
+ rule->filter_inner_pbit = (word >> 28) & 0xf;
+ rule->filter_inner_vid = (word >> 15) & 0x1fff;
+ rule->filter_inner_tpid_dei = (word >> 12) & 0x7;
+ rule->filter_ethertype = word & 0xf;
+
+ word = get_unaligned_be32(raw + 8);
+ rule->tags_to_remove = (word >> 30) & 0x3;
+ rule->treat_outer_pbit = (word >> 16) & 0xf;
+ rule->treat_outer_vid = (word >> 3) & 0x1fff;
+ rule->treat_outer_tpid_dei = word & 0x7;
+
+ word = get_unaligned_be32(raw + 12);
+ rule->treat_inner_pbit = (word >> 16) & 0xf;
+ rule->treat_inner_vid = (word >> 3) & 0x1fff;
+ rule->treat_inner_tpid_dei = word & 0x7;
+ rule->delete = omci_ext_vlan_rule_is_delete(raw);
+}
+
+static int omci_ext_vlan_update_rule(struct omci_extended_vlan *vlan,
+     const u8 *raw)
+{
+ unsigned int index;
+ bool delete;
+
+ delete = omci_ext_vlan_rule_is_delete(raw);
+ for (index = 0; index < vlan->rule_count; index++)
+ if (!memcmp(vlan->rules[index].raw, raw, 8))
+ break;
+
+ if (delete) {
+ if (index == vlan->rule_count)
+ return 0;
+ memmove(&vlan->rules[index], &vlan->rules[index + 1],
+ (vlan->rule_count - index - 1) * sizeof(vlan->rules[0]));
+ vlan->rule_count--;
+ memset(&vlan->rules[vlan->rule_count], 0,
+       sizeof(vlan->rules[0]));
+ return 0;
+ }
+
+ if (index == vlan->rule_count) {
+ if (vlan->rule_count >= OMCI_EXT_VLAN_MAX_RULES)
+ return -ENOSPC;
+ vlan->rule_count++;
+ }
+ omci_ext_vlan_parse_rule(&vlan->rules[index], raw);
+
+ return 0;
+}
+
+static void omci_ext_vlan_parse_create(struct omci_mib_object *object,
+       const u8 *content)
+{
+ struct omci_extended_vlan *vlan = &object->extended_vlan;
+
+ vlan->association_type = content[0];
+ vlan->associated_me = get_unaligned_be16(content + 24);
+ vlan->max_table_size = OMCI_EXT_VLAN_MAX_RULES;
+ vlan->valid = true;
+}
+
+static const u8 *omci_ext_vlan_consume(const u8 **data, size_t *len,
+       size_t size)
+{
+ const u8 *value;
+
+ if (*len < size)
+ return NULL;
+ value = *data;
+ *data += size;
+ *len -= size;
+
+ return value;
+}
+
+static int omci_ext_vlan_parse_set(struct omci_mib_object *object, u16 mask,
+   const u8 *data, size_t len)
+{
+ struct omci_extended_vlan *vlan = &object->extended_vlan;
+
+ if (mask & OMCI_EXT_VLAN_ASSOC_TYPE_MASK) {
+ const u8 *value = omci_ext_vlan_consume(&data, &len, 1);
+
+ if (!value)
+ return -EINVAL;
+ vlan->association_type = value[0];
+ }
+ if (mask & OMCI_EXT_VLAN_MAX_SIZE_MASK) {
+ const u8 *value = omci_ext_vlan_consume(&data, &len, 2);
+
+ if (!value)
+ return -EINVAL;
+ vlan->max_table_size = get_unaligned_be16(value);
+ }
+ if (mask & OMCI_EXT_VLAN_INPUT_TPID_MASK) {
+ const u8 *value = omci_ext_vlan_consume(&data, &len, 2);
+
+ if (!value)
+ return -EINVAL;
+ vlan->input_tpid = get_unaligned_be16(value);
+ }
+ if (mask & OMCI_EXT_VLAN_OUTPUT_TPID_MASK) {
+ const u8 *value = omci_ext_vlan_consume(&data, &len, 2);
+
+ if (!value)
+ return -EINVAL;
+ vlan->output_tpid = get_unaligned_be16(value);
+ }
+ if (mask & OMCI_EXT_VLAN_DOWNSTREAM_MASK) {
+ const u8 *value = omci_ext_vlan_consume(&data, &len, 1);
+
+ if (!value)
+ return -EINVAL;
+ vlan->downstream_mode = value[0];
+ }
+ if (mask & OMCI_EXT_VLAN_TABLE_MASK) {
+ const u8 *value = omci_ext_vlan_consume(&data, &len,
+ OMCI_EXT_VLAN_RULE_LEN);
+ int ret;
+
+ if (!value)
+ return -EINVAL;
+ ret = omci_ext_vlan_update_rule(vlan, value);
+ if (ret)
+ return ret;
+ }
+ if (mask & OMCI_EXT_VLAN_ASSOC_PTR_MASK) {
+ const u8 *value = omci_ext_vlan_consume(&data, &len, 2);
+
+ if (!value)
+ return -EINVAL;
+ vlan->associated_me = get_unaligned_be16(value);
+ }
+ if (mask & OMCI_EXT_VLAN_DSCP_MAP_MASK) {
+ const u8 *value = omci_ext_vlan_consume(&data, &len,
+ sizeof(vlan->dscp_to_pbit));
+
+ if (!value)
+ return -EINVAL;
+ memcpy(vlan->dscp_to_pbit, value, sizeof(vlan->dscp_to_pbit));
+ }
+ vlan->valid = true;
+
+ return 0;
+}
+
+struct omci_attr_layout {
+ u16 mask;
+ u8 offset;
+ u8 length;
+};
+
+static const struct omci_attr_layout omci_priority_queue_layout[] = {
+ { OMCI_PRIORITY_QUEUE_QUEUE_CONFIG_MASK, 0, 1 },
+ { OMCI_PRIORITY_QUEUE_MAX_SIZE_MASK, 1, 2 },
+ { OMCI_PRIORITY_QUEUE_ALLOC_SIZE_MASK, 3, 2 },
+ { OMCI_PRIORITY_QUEUE_DISCARD_RESET_MASK, 5, 2 },
+ { OMCI_PRIORITY_QUEUE_DISCARD_THRESHOLD_MASK, 7, 2 },
+ { OMCI_PRIORITY_QUEUE_RELATED_PORT_MASK, 9, 4 },
+ { OMCI_PRIORITY_QUEUE_SCHEDULER_MASK, 13, 2 },
+ { OMCI_PRIORITY_QUEUE_WEIGHT_MASK, 15, 1 },
+ { OMCI_PRIORITY_QUEUE_BACKPRESSURE_OP_MASK, 16, 2 },
+ { OMCI_PRIORITY_QUEUE_BACKPRESSURE_TIME_MASK, 18, 4 },
+ { OMCI_PRIORITY_QUEUE_BP_OCCUR_MASK, 22, 2 },
+ { OMCI_PRIORITY_QUEUE_BP_CLEAR_MASK, 24, 2 },
+};
+
+static const struct omci_attr_layout omci_traffic_scheduler_layout[] = {
+ { OMCI_TRAFFIC_SCHEDULER_TCONT_MASK, 0, 2 },
+ { OMCI_TRAFFIC_SCHEDULER_PARENT_MASK, 2, 2 },
+ { OMCI_TRAFFIC_SCHEDULER_POLICY_MASK, 4, 1 },
+ { OMCI_TRAFFIC_SCHEDULER_PRIORITY_MASK, 5, 1 },
+};
+
+static int omci_attr_parse_set(struct omci_mib_object *object, u16 mask,
+       const u8 *data, size_t len,
+       const struct omci_attr_layout *layout,
+       size_t layout_count, u16 supported_mask)
+{
+ size_t i;
+
+ if (mask & ~supported_mask)
+ return -EINVAL;
+
+ for (i = 0; i < layout_count; i++) {
+ if (!(mask & layout[i].mask))
+ continue;
+ if (len < layout[i].length)
+ return -EINVAL;
+ memcpy(object->data + layout[i].offset, data,
+       layout[i].length);
+ data += layout[i].length;
+ len -= layout[i].length;
+ }
+
+ return 0;
+}
+
+static int omci_attr_serialize(const struct omci_mib_object *object, u16 mask,
+       u8 *data, size_t len, size_t *encoded_len,
+       const struct omci_attr_layout *layout,
+       size_t layout_count, u16 supported_mask)
+{
+ size_t used = 0;
+ size_t i;
+
+ if (mask & ~supported_mask)
+ return -EINVAL;
+
+ for (i = 0; i < layout_count; i++) {
+ if (!(mask & layout[i].mask))
+ continue;
+ if (len - used < layout[i].length)
+ return -ENOSPC;
+ memcpy(data + used, object->data + layout[i].offset,
+       layout[i].length);
+ used += layout[i].length;
+ }
+
+ *encoded_len = used;
+ return 0;
+}
+
+static int
+omci_priority_queue_serialize(const struct omci_mib_object *object,
+      u16 mask, u8 *data, size_t len,
+      size_t *encoded_len)
+{
+ return omci_attr_serialize(object, mask, data, len, encoded_len,
+   omci_priority_queue_layout,
+   ARRAY_SIZE(omci_priority_queue_layout),
+   OMCI_PRIORITY_QUEUE_ATTR_MASK);
+}
+
+static int
+omci_traffic_scheduler_serialize(const struct omci_mib_object *object,
+ u16 mask, u8 *data, size_t len,
+ size_t *encoded_len)
+{
+ return omci_attr_serialize(object, mask, data, len, encoded_len,
+   omci_traffic_scheduler_layout,
+   ARRAY_SIZE(omci_traffic_scheduler_layout),
+   OMCI_TRAFFIC_SCHEDULER_ATTR_MASK);
+}
+
+static int omci_mib_parse_create(struct omci_mib_object *object,
+ const u8 *content)
+{
+ switch (object->class_id) {
+ case OMCI_CLASS_VLAN_TAGGING_FILTER_DATA:
+ omci_vlan_filter_parse_create(object, content);
+ break;
+ case OMCI_CLASS_EXTENDED_VLAN:
+ omci_ext_vlan_parse_create(object, content);
+ break;
+ default:
+ break;
+ }
+
+ return 0;
+}
+
+static int omci_mib_parse_set(struct omci_mib_object *object, u16 mask,
+      const u8 *data, size_t len)
+{
+ switch (object->class_id) {
+ case OMCI_CLASS_OLT_G:
+ return omci_olt_g_parse_set(object, mask, data, len);
+ case OMCI_CLASS_VLAN_TAGGING_FILTER_DATA:
+ return omci_vlan_filter_parse_set(object, mask, data, len);
+ case OMCI_CLASS_EXTENDED_VLAN:
+ return omci_ext_vlan_parse_set(object, mask, data, len);
+ case OMCI_CLASS_PRIORITY_QUEUE:
+ return omci_attr_parse_set(object, mask, data, len,
+   omci_priority_queue_layout,
+   ARRAY_SIZE(omci_priority_queue_layout),
+   OMCI_PRIORITY_QUEUE_ATTR_MASK);
+ case OMCI_CLASS_TRAFFIC_SCHEDULER:
+ return omci_attr_parse_set(object, mask, data, len,
+   omci_traffic_scheduler_layout,
+   ARRAY_SIZE(omci_traffic_scheduler_layout),
+   OMCI_TRAFFIC_SCHEDULER_ATTR_MASK);
+ default:
+ return 0;
+ }
+}
+
+static unsigned long omci_mib_key(u16 class_id, u16 entity_id)
+{
+ return ((unsigned long)class_id << 16) | entity_id;
+}
+
+static struct omci_mib_object *
+omci_mib_lookup(struct omci_agent *agent, u16 class_id, u16 entity_id)
+{
+ struct omci_mib_object *object;
+
+ object = xa_load(&agent->mib, omci_mib_key(class_id, entity_id));
+ if (object && object->pending_delete)
+ return NULL;
+
+ return object;
+}
+
+static void omci_agent_set_mib_sync_locked(struct omci_agent *agent, u8 sync)
+{
+ struct omci_mib_object *object;
+
+ agent->mib_sync = sync;
+ object = omci_mib_lookup(agent, OMCI_CLASS_ONU_DATA, 0);
+ if (object)
+ object->data[0] = sync;
+}
+
+static void omci_agent_increment_mib_sync_locked(struct omci_agent *agent)
+{
+ omci_agent_set_mib_sync_locked(agent, (u8)(agent->mib_sync + 1));
+}
+
+static int omci_mib_store_locked(struct omci_agent *agent,
+ const struct omci_mib_object *source)
+{
+ struct omci_mib_object *object;
+ void *old;
+
+ object = kmemdup(source, sizeof(*object), GFP_KERNEL);
+ if (!object)
+ return -ENOMEM;
+
+ old = xa_store(&agent->mib,
+       omci_mib_key(source->class_id, source->entity_id),
+       object, GFP_KERNEL);
+ if (xa_is_err(old)) {
+ kfree(object);
+ return xa_err(old);
+ }
+ kfree(old);
+ return 0;
+}
+
+static struct omci_olt_g *omci_agent_olt_g_locked(struct omci_agent *agent)
+{
+ struct omci_mib_object *object;
+
+ object = omci_mib_lookup(agent, OMCI_CLASS_OLT_G, 0);
+ if (!object || !object->olt_g.valid)
+ return NULL;
+
+ return &object->olt_g;
+}
+
+int omci_agent_olt_g_get(struct omci_device *odev, struct omci_olt_g *olt)
+{
+ struct omci_olt_g *stored;
+ int ret = 0;
+
+ if (!olt)
+ return -EINVAL;
+
+ mutex_lock(&odev->agent.lock);
+ stored = omci_agent_olt_g_locked(&odev->agent);
+ if (!stored)
+ ret = -ENODATA;
+ else
+ *olt = *stored;
+ mutex_unlock(&odev->agent.lock);
+
+ return ret;
+}
+
+static void
+omci_agent_profile_state_locked(struct omci_agent *agent,
+ const struct omci_olt_g *olt,
+ struct omci_olt_profile_state *state)
+{
+ u8 effective;
+
+ memset(state, 0, sizeof(*state));
+ state->configured = agent->config.olt_profile;
+ state->forced = agent->config.olt_profile_force;
+
+ if (state->forced != OMCI_OLT_PROFILE_UNSPEC)
+ effective = state->forced;
+ else if (state->configured == OMCI_OLT_PROFILE_AUTO)
+ effective = omci_profile_detect(olt);
+ else
+ effective = state->configured;
+
+ if (effective == OMCI_OLT_PROFILE_UNSPEC ||
+    effective == OMCI_OLT_PROFILE_AUTO)
+ effective = OMCI_OLT_PROFILE_GENERIC;
+
+ state->effective = effective;
+ state->quirks = omci_profile_quirks(effective);
+ if (olt)
+ state->olt = *olt;
+}
+
+static int
+omci_agent_profile_reconcile_locked(struct omci_device *odev,
+    u8 old_profile, u8 new_profile);
+static int omci_agent_clear_services_locked(struct omci_device *odev);
+static int omci_agent_reconcile_services_locked(struct omci_device *odev);
+static void omci_agent_reset_duplicate_locked(struct omci_agent *agent);
+
+static void omci_agent_free_object_array(struct xarray *objects)
+{
+ struct omci_mib_object *object;
+ unsigned long index;
+
+ xa_for_each(objects, index, object) {
+ xa_erase(objects, index);
+ kfree(object);
+ }
+}
+
+static int omci_agent_snapshot_vendor_objects_locked(struct omci_agent *agent,
+      struct xarray *snapshot)
+{
+ struct omci_mib_object *object;
+ struct omci_mib_object *copy;
+ unsigned long index;
+ void *old;
+
+ xa_for_each(&agent->mib, index, object) {
+ if (!object->owner_profile)
+ continue;
+ copy = kmemdup(object, sizeof(*copy), GFP_KERNEL);
+ if (!copy)
+ return -ENOMEM;
+ old = xa_store(snapshot, index, copy, GFP_KERNEL);
+ if (xa_is_err(old)) {
+ kfree(copy);
+ return xa_err(old);
+ }
+ kfree(old);
+ }
+
+ return 0;
+}
+
+static int omci_agent_restore_vendor_objects_locked(struct omci_agent *agent,
+     struct xarray *snapshot)
+{
+ struct omci_mib_object *object;
+ unsigned long index;
+ void *old;
+ int ret = 0;
+
+ xa_for_each(&agent->mib, index, object) {
+ if (!object->owner_profile)
+ continue;
+ xa_erase(&agent->mib, index);
+ kfree(object);
+ }
+ xa_for_each(snapshot, index, object) {
+ xa_erase(snapshot, index);
+ old = xa_store(&agent->mib, index, object, GFP_KERNEL);
+ if (xa_is_err(old)) {
+ kfree(object);
+ if (!ret)
+ ret = xa_err(old);
+ continue;
+ }
+ kfree(old);
+ }
+
+ return ret;
+}
+
+static int
+omci_agent_profile_refresh_locked(struct omci_device *odev,
+  struct omci_olt_g *olt,
+  bool *profile_changed)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_olt_profile_state old_state;
+ struct omci_olt_profile_state state;
+ struct xarray snapshot;
+ u32 old_quirks = agent->profile_quirks;
+ u8 old_profile = agent->profile_effective;
+ bool changed;
+ int ret;
+
+ omci_agent_profile_state_locked(agent, olt, &state);
+ omci_profile_sanitize_olt_g(olt, state.quirks);
+ if (olt)
+ state.olt = *olt;
+
+ changed = old_profile != state.effective || old_quirks != state.quirks;
+ if (!changed)
+ goto apply_profile;
+
+ xa_init(&snapshot);
+ ret = omci_agent_snapshot_vendor_objects_locked(agent, &snapshot);
+ if (ret)
+ goto destroy_snapshot;
+ ret = omci_agent_profile_reconcile_locked(odev, old_profile,
+  state.effective);
+ if (ret)
+ goto rollback;
+
+apply_profile:
+ if (odev->ops->set_olt_profile) {
+ ret = odev->ops->set_olt_profile(odev, &state);
+ if (ret && changed)
+ goto rollback;
+ if (ret)
+ return ret;
+ }
+
+ agent->profile_effective = state.effective;
+ agent->profile_quirks = state.quirks;
+ if (profile_changed)
+ *profile_changed = changed;
+
+ if (changed) {
+ omci_agent_reset_table_snapshot_locked(agent);
+ agent->upload_index = 0;
+ omci_agent_free_object_array(&snapshot);
+ xa_destroy(&snapshot);
+ dev_info(odev->parent,
+ "OMCI OLT profile: configured=%s effective=%s forced=%s quirks=%#x\n",
+ omci_olt_profile_name(state.configured),
+ omci_olt_profile_name(state.effective),
+ omci_olt_profile_name(state.forced), state.quirks);
+ }
+
+ return 0;
+
+rollback:
+ omci_agent_restore_vendor_objects_locked(agent, &snapshot);
+ agent->profile_effective = old_profile;
+ agent->profile_quirks = old_quirks;
+ omci_agent_reconcile_services_locked(odev);
+ if (odev->ops->set_olt_profile) {
+ old_state = state;
+ old_state.effective = old_profile;
+ old_state.quirks = old_quirks;
+ odev->ops->set_olt_profile(odev, &old_state);
+ }
+ omci_agent_free_object_array(&snapshot);
+
+destroy_snapshot:
+ xa_destroy(&snapshot);
+ return ret;
+}
+
+static bool
+omci_agent_profile_has_quirk(const struct omci_agent *agent, u32 quirk)
+{
+ return agent->profile_quirks & quirk;
+}
+
+static bool
+omci_agent_fakes_unsupported(const struct omci_agent *agent)
+{
+ return agent->fake_omci ||
+       omci_agent_profile_has_quirk(agent,
+    OMCI_OLT_QUIRK_FAKE_UNSUPPORTED_SUCCESS);
+}
+
+static bool
+omci_agent_should_fake_result(const struct omci_agent *agent, u16 class_id,
+      u8 result)
+{
+ const struct omci_me_desc *desc = omci_me_lookup(agent, class_id);
+
+ if (desc && (desc->flags & OMCI_ME_F_DATAPATH))
+ return false;
+
+ return omci_agent_fakes_unsupported(agent) &&
+       omci_agent_result_can_be_faked(result);
+}
+
+static int
+omci_mib_add_default_profile_mask(struct omci_agent *agent, u8 profile,
+  u16 class_id, u16 entity_id,
+  u16 attr_mask, const void *data,
+  size_t len)
+{
+ const struct omci_me_desc *desc;
+ struct omci_mib_object *object;
+ int ret;
+
+ desc = omci_me_lookup_profile(profile, class_id);
+ object = kzalloc(sizeof(*object), GFP_KERNEL);
+ if (!object)
+ return -ENOMEM;
+
+ object->class_id = class_id;
+ object->entity_id = entity_id;
+ object->attr_mask = attr_mask;
+ object->origin = OMCI_MIB_ORIGIN_DEFAULT;
+ object->owner_profile = desc && (desc->flags & OMCI_ME_F_VENDOR) ?
+ profile : OMCI_OLT_PROFILE_UNSPEC;
+ object->data_len = desc ? desc->data_len :
+ min_t(size_t, len, sizeof(object->data));
+ if (data)
+ memcpy(object->data, data, min(len, sizeof(object->data)));
+
+ ret = omci_mib_store_locked(agent, object);
+ kfree(object);
+ return ret;
+}
+
+static int omci_mib_add_default_mask(struct omci_agent *agent, u16 class_id,
+     u16 entity_id, u16 attr_mask,
+     const void *data, size_t len)
+{
+ u8 profile = agent->profile_effective;
+
+ if (!profile)
+ profile = OMCI_OLT_PROFILE_GENERIC;
+
+ return omci_mib_add_default_profile_mask(agent, profile, class_id,
+ entity_id, attr_mask, data, len);
+}
+
+static int omci_mib_add_default(struct omci_agent *agent, u16 class_id,
+ u16 entity_id, const void *data, size_t len)
+{
+ return omci_mib_add_default_mask(agent, class_id, entity_id, 0xffff,
+ data, len);
+}
+
+static int
+omci_agent_seed_huawei_locked(struct omci_device *odev, u8 profile)
+{
+ struct omci_agent *agent = &odev->agent;
+ u8 data[OMCI_MAX_ATTR_DATA] = {};
+ int ret;
+
+ data[1] = agent->config.traffic_mgmt_option;
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_HUAWEI_FLOW_MAPPING, 0, GENMASK(15, 7),
+ data, sizeof(data));
+ if (ret)
+ return ret;
+
+ memset(data, 0, sizeof(data));
+ memcpy(data, agent->config.version, min_t(size_t, 14,
+ sizeof(agent->config.version)));
+ data[14] = 1;
+ data[15] = 1;
+ data[16] = 1;
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_HUAWEI_SW_IMAGE_EXT, 0, GENMASK(15, 11),
+ data, sizeof(data));
+ if (ret)
+ return ret;
+
+ memset(data, 0, sizeof(data));
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_HUAWEI_MULTICAST_367, 0, GENMASK(15, 11),
+ data, sizeof(data));
+ if (ret)
+ return ret;
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_HUAWEI_MULTICAST_370, 0x0101,
+ GENMASK(15, 11), data, sizeof(data));
+ if (ret)
+ return ret;
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_HUAWEI_MULTICAST_373, 0, GENMASK(15, 11),
+ data, sizeof(data));
+ if (ret)
+ return ret;
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_HUAWEI_MULTICAST_65408, 0, GENMASK(15, 11),
+ data, sizeof(data));
+ if (ret)
+ return ret;
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_HUAWEI_MULTICAST_65414, 0, GENMASK(15, 11),
+ data, sizeof(data));
+ if (ret)
+ return ret;
+
+ return omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_HUAWEI_MULTICAST_65425, 0, GENMASK(15, 11),
+ data, sizeof(data));
+}
+
+static int
+omci_agent_seed_nokia_locked(struct omci_device *odev, u8 profile)
+{
+ struct omci_agent *agent = &odev->agent;
+ u8 data[OMCI_MAX_ATTR_DATA] = {};
+ unsigned int i;
+ int ret;
+
+ data[0] = 1;
+ data[3] = 1;
+ data[6] = 1;
+ data[9] = 1;
+ data[12] = 1;
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_NOKIA_OPTICAL_SUPERVISION,
+ OMCI_ANI_G_ENTITY_ID,
+ GENMASK(15, 6), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ memset(data, 0, sizeof(data));
+ memcpy(data + 15, agent->config.vendor_id,
+       sizeof(agent->config.vendor_id));
+ memcpy(data + 19, agent->config.serial_number,
+       sizeof(agent->config.serial_number));
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_NOKIA_ONT_GENERIC_V2, 0,
+ GENMASK(15, 4), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ for (i = 0; i < agent->config.uni_count; i++) {
+ memset(data, 0, sizeof(data));
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_NOKIA_UNI_SUPPLEMENTAL_V2,
+ OMCI_UNI_ENTITY_ID(OMCI_ETHERNET_SLOT,
+   i + 1),
+ GENMASK(15, 9), data, sizeof(data));
+ if (ret)
+ return ret;
+ }
+
+ /*
+ * In Nokia GPON HGU / VEIP mode, the OLT assigns Alloc-ID 256 and 257
+ * along with GEM ports 256 and 257 via PLOAM without OMCI Create messages.
+ * Seed the standard VEIP datapath bridge for both GEM 256 and GEM 257.
+ */
+ if (agent->config.onu_type == 2) {
+ /* T-CONT 0x8000 -> Alloc-ID 256 */
+ memset(data, 0, sizeof(data));
+ put_unaligned_be16(256, data);
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_TCONT, 0x8000,
+ BIT(15), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ /* GEM Port Network CTP 256 -> port 256, T-CONT 0x8000, bidir(3) */
+ memset(data, 0, sizeof(data));
+ put_unaligned_be16(256, data);
+ put_unaligned_be16(0x8000, data + 2);
+ data[4] = 3;
+ put_unaligned_be16(0x8000, data + 5);
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_GEM_PORT_CTP, 256,
+ GENMASK(15, 6), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ /*
+ * GEM IWTP 256 -> GEM CTP 256. Interworking option 5 is
+ * "802.1p mapper", so the service profile pointer at offset 3
+ * has to name a real class 130 instance; leaving it zero
+ * advertises mapper interworking with no mapper behind it.
+ */
+ memset(data, 0, sizeof(data));
+ put_unaligned_be16(256, data);
+ data[2] = 5;
+ put_unaligned_be16(OMCI_NOKIA_MAPPER_MGMT, data + 3);
+ put_unaligned_be16(0x0a01, data + 5);
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_GEM_IWTP, 256,
+ GENMASK(15, 8), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ /* T-CONT 0x8001 -> Alloc-ID 257 */
+ memset(data, 0, sizeof(data));
+ put_unaligned_be16(257, data);
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_TCONT, 0x8001,
+ BIT(15), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ /* GEM Port Network CTP 257 -> port 257, T-CONT 0x8001, bidir(3) */
+ memset(data, 0, sizeof(data));
+ put_unaligned_be16(257, data);
+ put_unaligned_be16(0x8001, data + 2);
+ data[4] = 3;
+ put_unaligned_be16(0x8001, data + 5);
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_GEM_PORT_CTP, 257,
+ GENMASK(15, 6), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ /* GEM IWTP 257 -> GEM CTP 257, via 802.1p mapper 2. */
+ memset(data, 0, sizeof(data));
+ put_unaligned_be16(257, data);
+ data[2] = 5;
+ put_unaligned_be16(OMCI_NOKIA_MAPPER_HSI, data + 3);
+ put_unaligned_be16(0x0a01, data + 5);
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_GEM_IWTP, 257,
+ GENMASK(15, 8), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ /*
+ * 802.1p mappers, one per GEM interworking TP. A reference ONU
+ * carrying service on this OLT points every P-bit at the same
+ * interworking TP, so do the same rather than invent a
+ * priority split the OLT never asked for.
+ *
+ * Layout (G.988 9.3.10): TP pointer at 0, then the eight P-bit
+ * interworking TP pointers, unmarked frame option at 18, DSCP
+ * map at 19, default P-bit marking at 43, TP type at 44.
+ */
+ for (i = 0; i < 2; i++) {
+ u16 mapper = i ? OMCI_NOKIA_MAPPER_HSI :
+ OMCI_NOKIA_MAPPER_MGMT;
+ u16 iwtp = i ? 257 : 256;
+ unsigned int pbit;
+
+ memset(data, 0, sizeof(data));
+ put_unaligned_be16(0xffff, data);
+ for (pbit = 0; pbit < 8; pbit++)
+ put_unaligned_be16(iwtp, data + 2 + pbit * 2);
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_8021P_MAPPER, mapper,
+ GENMASK(15, 3), data, sizeof(data));
+ if (ret)
+ return ret;
+ }
+
+ /* MAC Bridge Service Profile 1 */
+ memset(data, 0, sizeof(data));
+ data[0] = 1;
+ data[1] = 1;
+ data[2] = 1;
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_MAC_BRIDGE_SERVICE_PROFILE, 1,
+ GENMASK(15, 6), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ /* MAC Bridge Port Config 1 -> VEIP (TP type 11, slot 6) */
+ memset(data, 0, sizeof(data));
+ put_unaligned_be16(1, data);
+ data[2] = 1;
+ data[3] = 11;
+ put_unaligned_be16(OMCI_UNI_ENTITY_ID(6, 1), data + 4);
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_MAC_BRIDGE_PORT_CONFIG_DATA, 1,
+ GENMASK(15, 12), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ /*
+ * MAC Bridge Port 2 -> 802.1p mapper 2 (Internet / HSI).
+ * TP type 3 is "802.1p mapper"; binding the port straight to
+ * the GEM interworking TP with type 5 skips the mapper layer a
+ * working reference ONU on this OLT uses.
+ */
+ memset(data, 0, sizeof(data));
+ put_unaligned_be16(1, data);
+ data[2] = 2;
+ data[3] = 3;
+ put_unaligned_be16(OMCI_NOKIA_MAPPER_HSI, data + 4);
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_MAC_BRIDGE_PORT_CONFIG_DATA, 2,
+ GENMASK(15, 12), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ /* MAC Bridge Port 3 -> 802.1p mapper 1 (management / VoIP). */
+ memset(data, 0, sizeof(data));
+ put_unaligned_be16(1, data);
+ data[2] = 3;
+ data[3] = 3;
+ put_unaligned_be16(OMCI_NOKIA_MAPPER_MGMT, data + 4);
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_MAC_BRIDGE_PORT_CONFIG_DATA, 3,
+ GENMASK(15, 12), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ /*
+ * Class 84 (VLAN tagging filter data), one per mapper: VID 100
+ * for high speed internet and VID 660 for management and VoIP.
+ * data[24] is the forward operation, data[25] the number of
+ * valid entries in the filter list.
+ */
+ memset(data, 0, sizeof(data));
+ put_unaligned_be16(100, data);
+ data[24] = 0x10;
+ data[25] = 1;
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_VLAN_TAGGING_FILTER_DATA,
+ OMCI_NOKIA_MAPPER_HSI,
+ GENMASK(15, 13), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ memset(data, 0, sizeof(data));
+ put_unaligned_be16(660, data);
+ data[24] = 0x10;
+ data[25] = 1;
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_VLAN_TAGGING_FILTER_DATA,
+ OMCI_NOKIA_MAPPER_MGMT,
+ GENMASK(15, 13), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ /*
+ * Class 171 (extended VLAN tagging operation configuration
+ * data) on both VEIP instances, association type 10 (VEIP).
+ * G.988 9.3.13 layout: association type at 0, table size at 1,
+ * input and output TPID at 3 and 5, downstream mode at 7 and
+ * the associated ME pointer at 24.
+ */
+ memset(data, 0, sizeof(data));
+ data[0] = 10;
+ put_unaligned_be16(OMCI_EXT_VLAN_MAX_RULES, data + 1);
+ put_unaligned_be16(ETH_P_8021Q, data + 3);
+ put_unaligned_be16(ETH_P_8021Q, data + 5);
+ data[7] = 0;
+ put_unaligned_be16(OMCI_UNI_ENTITY_ID(6, 1), data + 24);
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_EXTENDED_VLAN,
+ OMCI_UNI_ENTITY_ID(6, 1),
+ GENMASK(15, 10), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ put_unaligned_be16(OMCI_UNI_ENTITY_ID(OMCI_VEIP_SLOT, 1),
+   data + 24);
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_EXTENDED_VLAN,
+ OMCI_UNI_ENTITY_ID(OMCI_VEIP_SLOT, 1),
+ GENMASK(15, 10), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ /* Class 131 (OLT-G): pre-seed the Nokia/ALCL OLT identity. */
+ memset(data, 0, sizeof(data));
+ memcpy(data, "ALCL", 4);
+ memcpy(data + 4, "ISAM                ", 20);
+ ret = omci_mib_add_default_profile_mask(agent, profile,
+ OMCI_CLASS_OLT_G, 0,
+ GENMASK(15, 14), data, sizeof(data));
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int
+omci_agent_seed_profile_locked(struct omci_device *odev, u8 profile)
+{
+ switch (profile) {
+ case OMCI_OLT_PROFILE_HUAWEI:
+ return omci_agent_seed_huawei_locked(odev, profile);
+ case OMCI_OLT_PROFILE_NOKIA_ALCL:
+ return omci_agent_seed_nokia_locked(odev, profile);
+ default:
+ return 0;
+ }
+}
+
+static int
+omci_agent_profile_reconcile_locked(struct omci_device *odev,
+    u8 old_profile, u8 new_profile)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_mib_object *object;
+ unsigned long index;
+ int ret;
+
+ if (old_profile == new_profile)
+ return 0;
+
+ xa_for_each(&agent->mib, index, object) {
+ if (!object->owner_profile || object->owner_profile == new_profile)
+ continue;
+ xa_erase(&agent->mib, index);
+ kfree(object);
+ }
+
+ ret = omci_agent_seed_profile_locked(odev, new_profile);
+ if (ret)
+ return ret;
+ agent->profile_effective = new_profile;
+ ret = omci_agent_reconcile_services_locked(odev);
+ agent->profile_effective = old_profile;
+ if (ret && ret != -EOPNOTSUPP)
+ return ret;
+
+ return 0;
+}
+
+static int
+omci_mib_add_priority_queue_default(struct omci_agent *agent,
+    u16 entity_id, const u8 *data)
+{
+ return omci_mib_add_default_mask(agent, OMCI_CLASS_PRIORITY_QUEUE,
+ entity_id,
+ OMCI_PRIORITY_QUEUE_ATTR_MASK, data,
+ OMCI_MAX_ATTR_DATA);
+}
+
+static int
+omci_mib_add_traffic_scheduler_default(struct omci_agent *agent,
+       u16 entity_id, const u8 *data)
+{
+ return omci_mib_add_default_mask(agent, OMCI_CLASS_TRAFFIC_SCHEDULER,
+ entity_id,
+ OMCI_TRAFFIC_SCHEDULER_ATTR_MASK,
+ data, OMCI_MAX_ATTR_DATA);
+}
+
+static u16 omci_mib_count_class(struct omci_agent *agent, u16 class_id)
+{
+ struct omci_mib_object *object;
+ unsigned long index;
+ u16 count = 0;
+
+ xa_for_each(&agent->mib, index, object) {
+ if (object->class_id == class_id && count < U16_MAX)
+ count++;
+ }
+
+ return count;
+}
+
+static void omci_agent_refresh_identity_locked(struct omci_agent *agent)
+{
+ struct omci_mib_object *object;
+ unsigned long index;
+ u16 queues, schedulers;
+
+ xa_for_each(&agent->mib, index, object) {
+ if (object->class_id == OMCI_CLASS_SOFTWARE_IMAGE &&
+    object->entity_id < ARRAY_SIZE(agent->config.software_version)) {
+ memcpy(object->data,
+       agent->config.software_version[object->entity_id],
+       OMCI_SOFTWARE_VERSION_LEN);
+ } else if (object->class_id == OMCI_CLASS_CARDHOLDER) {
+ memcpy(object->data + 3, agent->config.equipment_id,
+       sizeof(agent->config.equipment_id));
+ memcpy(object->data + 23, agent->config.equipment_id,
+       sizeof(agent->config.equipment_id));
+ } else if (object->class_id == OMCI_CLASS_CIRCUIT_PACK) {
+ memcpy(object->data + 2, agent->config.serial_number,
+       sizeof(agent->config.serial_number));
+ memcpy(object->data + 10, agent->config.version,
+       sizeof(agent->config.version));
+ memcpy(object->data + 24, agent->config.vendor_id,
+       sizeof(agent->config.vendor_id));
+ memcpy(object->data + 31, agent->config.equipment_id,
+       sizeof(agent->config.equipment_id));
+ }
+ }
+
+ object = omci_mib_lookup(agent, OMCI_CLASS_ONU_G, 0);
+ if (object) {
+ memset(object->data, 0, sizeof(object->data));
+ memcpy(object->data, agent->config.vendor_id,
+       sizeof(agent->config.vendor_id));
+ memcpy(object->data + 4, agent->config.version,
+       sizeof(agent->config.version));
+ memcpy(object->data + 18, agent->config.serial_number,
+       sizeof(agent->config.serial_number));
+ object->data[26] = agent->config.traffic_mgmt_option;
+ }
+
+ object = omci_mib_lookup(agent, OMCI_CLASS_ONU2_G, 0);
+ if (object) {
+ memset(object->data, 0, sizeof(object->data));
+ memcpy(object->data, agent->config.equipment_id,
+       min(sizeof(agent->config.equipment_id),
+   sizeof(object->data)));
+ object->data[20] = agent->config.omcc_version;
+ queues = omci_mib_count_class(agent, OMCI_CLASS_PRIORITY_QUEUE);
+ put_unaligned_be16(queues, object->data + 25);
+ schedulers = omci_mib_count_class(agent,
+  OMCI_CLASS_TRAFFIC_SCHEDULER);
+ object->data[27] = min_t(u16, schedulers, U8_MAX);
+ put_unaligned_be16(32, object->data + 29);
+ /*
+ * G.988 9.1.3. The OLT reads these to decide what it may
+ * provision. Left at zero they say the ONU supports no
+ * security and no connectivity model at all, and an OLT that
+ * honours that identifies the ONU and then has nothing it can
+ * legally create.
+ */
+ object->data[23] = OMCI_ONU2G_SECURITY_AES128;
+ object->data[24] = OMCI_ONU2G_SECURITY_AES128;
+ put_unaligned_be16(OMCI_ONU2G_CONNECTIVITY_CAPABILITY,
+   object->data + 35);
+ put_unaligned_be16(1, object->data + 40);
+ }
+
+ object = omci_mib_lookup(agent, OMCI_CLASS_HUAWEI_SW_IMAGE_EXT, 0);
+ if (object)
+ memcpy(object->data, agent->config.software_version[0],
+       min_t(size_t, 14, OMCI_SOFTWARE_VERSION_LEN));
+
+ object = omci_mib_lookup(agent, OMCI_CLASS_NOKIA_ONT_GENERIC_V2, 0);
+ if (object) {
+ memcpy(object->data + 15, agent->config.vendor_id,
+       sizeof(agent->config.vendor_id));
+ memcpy(object->data + 19, agent->config.serial_number,
+       sizeof(agent->config.serial_number));
+ }
+}
+
+static void omci_agent_apply_identity(struct omci_agent *agent,
+      const struct omci_identity *identity)
+{
+ if (identity->valid & OMCI_IDENTITY_F_SERIAL_NUMBER) {
+ memcpy(agent->config.serial_number, identity->serial_number,
+       sizeof(agent->config.serial_number));
+ agent->config.serial_source = identity->serial_source;
+ }
+ if (identity->valid & OMCI_IDENTITY_F_VENDOR_ID) {
+ memcpy(agent->config.vendor_id, identity->vendor_id,
+       sizeof(agent->config.vendor_id));
+ agent->config.vendor_source = identity->vendor_source;
+ }
+ if (identity->valid & OMCI_IDENTITY_F_PASSWORD) {
+ memcpy(agent->config.password, identity->password,
+       sizeof(agent->config.password));
+ agent->config.password_source = identity->password_source;
+ }
+ if (identity->valid & OMCI_IDENTITY_F_VERSION) {
+ memcpy(agent->config.version, identity->version,
+       sizeof(agent->config.version));
+ memcpy(agent->config.software_version[0], identity->version,
+       sizeof(agent->config.software_version[0]));
+ memcpy(agent->config.software_version[1], identity->version,
+       sizeof(agent->config.software_version[1]));
+ agent->config.version_source = identity->version_source;
+ agent->config.software_version_source[0] =
+ identity->version_source;
+ agent->config.software_version_source[1] =
+ identity->version_source;
+ }
+ if (identity->valid & OMCI_IDENTITY_F_EQUIPMENT_ID) {
+ memcpy(agent->config.equipment_id, identity->equipment_id,
+       sizeof(agent->config.equipment_id));
+ agent->config.equipment_source = identity->equipment_source;
+ }
+}
+
+static void
+omci_agent_identity_state_locked(const struct omci_agent *agent,
+ struct omci_identity *identity)
+{
+ memset(identity, 0, sizeof(*identity));
+ identity->valid = OMCI_IDENTITY_F_SERIAL_NUMBER |
+  OMCI_IDENTITY_F_VENDOR_ID |
+  OMCI_IDENTITY_F_PASSWORD |
+  OMCI_IDENTITY_F_VERSION |
+  OMCI_IDENTITY_F_EQUIPMENT_ID;
+ memcpy(identity->serial_number, agent->config.serial_number,
+       sizeof(identity->serial_number));
+ memcpy(identity->vendor_id, agent->config.vendor_id,
+       sizeof(identity->vendor_id));
+ memcpy(identity->password, agent->config.password,
+       sizeof(identity->password));
+ memcpy(identity->version, agent->config.version,
+       sizeof(identity->version));
+ memcpy(identity->equipment_id, agent->config.equipment_id,
+       sizeof(identity->equipment_id));
+ identity->serial_source = agent->config.serial_source;
+ identity->vendor_source = agent->config.vendor_source;
+ identity->password_source = agent->config.password_source;
+ identity->version_source = agent->config.version_source;
+ identity->equipment_source = agent->config.equipment_source;
+}
+
+static int omci_agent_get_ani_topology(struct omci_device *odev,
+       struct omci_ani_topology *topology)
+{
+ u32 last_tcont, last_scheduler, last_queue;
+ int ret = 0;
+
+ *topology = (struct omci_ani_topology) {
+ .tcont_base = 0x8000,
+ .scheduler_base = 0x8000,
+ .queue_base = 0x8000,
+ .maximum_queue_size = 0xffff,
+ .allocated_queue_size = 4,
+ .tcont_count = OMCI_DEFAULT_TCONT_COUNT,
+ .queues_per_tcont = OMCI_DEFAULT_QUEUES_PER_TCONT,
+ .queue_config_option = 1,
+ .scheduler_policy = 1,
+ };
+
+ if (odev->ops && odev->ops->get_ani_topology) {
+ ret = odev->ops->get_ani_topology(odev, topology);
+ if (ret)
+ return ret;
+ }
+
+ if (!topology->tcont_count ||
+    topology->tcont_count > OMCI_MAX_TCONT_RESOURCES ||
+    !topology->queues_per_tcont ||
+    topology->queues_per_tcont > OMCI_MAX_QUEUES_PER_TCONT)
+ return -EINVAL;
+
+ last_tcont = topology->tcont_base + topology->tcont_count - 1;
+ last_scheduler = topology->scheduler_base + topology->tcont_count - 1;
+ last_queue = topology->queue_base +
+     topology->tcont_count * topology->queues_per_tcont - 1;
+ if (last_tcont > U16_MAX || last_scheduler > U16_MAX ||
+    last_queue > U16_MAX)
+ return -ERANGE;
+
+ return 0;
+}
+
+static void
+omci_priority_queue_default_data(const struct omci_ani_topology *topology,
+ unsigned int tcont, unsigned int queue,
+ u8 data[OMCI_MAX_ATTR_DATA])
+{
+ u16 tcont_entity = topology->tcont_base + tcont;
+ u16 scheduler_entity = topology->scheduler_base + tcont;
+ u16 priority = topology->queues_per_tcont - queue - 1;
+
+ memset(data, 0, OMCI_MAX_ATTR_DATA);
+ data[0] = topology->queue_config_option;
+ put_unaligned_be16(topology->maximum_queue_size, data + 1);
+ put_unaligned_be16(topology->allocated_queue_size, data + 3);
+ put_unaligned_be16(tcont_entity, data + 9);
+ put_unaligned_be16(priority, data + 11);
+ put_unaligned_be16(scheduler_entity, data + 13);
+ data[15] = 1;
+ put_unaligned_be16(0xffff, data + 22);
+}
+
+static void
+omci_traffic_scheduler_default_data(const struct omci_ani_topology *topology,
+    unsigned int tcont,
+    u8 data[OMCI_MAX_ATTR_DATA])
+{
+ memset(data, 0, OMCI_MAX_ATTR_DATA);
+ put_unaligned_be16(topology->tcont_base + tcont, data);
+ data[4] = topology->scheduler_policy;
+}
+
+static void
+omci_cardholder_default_data(const struct omci_agent *agent, u8 unit_type,
+     u8 port_count, u8 data[OMCI_MAX_ATTR_DATA])
+{
+ memset(data, 0, OMCI_MAX_ATTR_DATA);
+ data[0] = unit_type;
+ data[1] = unit_type;
+ data[2] = port_count;
+ memcpy(data + 3, agent->config.equipment_id,
+       sizeof(agent->config.equipment_id));
+ memcpy(data + 23, agent->config.equipment_id,
+       sizeof(agent->config.equipment_id));
+}
+
+static void
+omci_circuit_pack_default_data(const struct omci_agent *agent,
+       const struct omci_ani_topology *topology,
+       u8 unit_type, u8 port_count, bool ani,
+       u8 data[OMCI_MAX_ATTR_DATA])
+{
+ u16 queues;
+
+ memset(data, 0, OMCI_MAX_ATTR_DATA);
+ data[0] = unit_type;
+ data[1] = port_count;
+ memcpy(data + 2, agent->config.serial_number,
+       sizeof(agent->config.serial_number));
+ memcpy(data + 10, agent->config.version,
+       sizeof(agent->config.version));
+ memcpy(data + 24, agent->config.vendor_id,
+       sizeof(agent->config.vendor_id));
+ /* Administrative and operational states are enabled. */
+ data[28] = 0;
+ data[29] = 0;
+ /* The default service model is bridged rather than routed. */
+ data[30] = 0;
+ memcpy(data + 31, agent->config.equipment_id,
+       sizeof(agent->config.equipment_id));
+ if (!ani)
+ return;
+
+ queues = topology->tcont_count * topology->queues_per_tcont;
+ data[52] = min_t(u16, topology->tcont_count, U8_MAX);
+ data[53] = min_t(u16, queues, U8_MAX);
+ data[54] = min_t(u16, topology->tcont_count, U8_MAX);
+}
+
+static int
+omci_agent_add_equipment_default(struct omci_agent *agent,
+ const struct omci_ani_topology *topology,
+ u8 slot, u8 unit_type, u8 port_count,
+ bool ani, u8 data[OMCI_MAX_ATTR_DATA])
+{
+ u16 entity_id = OMCI_EQUIPMENT_ENTITY_ID(slot);
+ int ret;
+
+ omci_cardholder_default_data(agent, unit_type, port_count, data);
+ ret = omci_mib_add_default_mask(agent, OMCI_CLASS_CARDHOLDER,
+ entity_id, GENMASK(15, 10), data,
+ OMCI_MAX_ATTR_DATA);
+ if (ret)
+ return ret;
+
+ omci_circuit_pack_default_data(agent, topology, unit_type, port_count,
+       ani, data);
+ return omci_mib_add_default_mask(agent, OMCI_CLASS_CIRCUIT_PACK,
+ entity_id, GENMASK(15, 2), data,
+ OMCI_MAX_ATTR_DATA);
+}
+
+static void omci_uni_g_default_data(u8 data[OMCI_MAX_ATTR_DATA])
+{
+ memset(data, 0, OMCI_MAX_ATTR_DATA);
+ /* Support both OMCI and non-OMCI management. */
+ data[3] = 2;
+}
+
+static int omci_agent_populate_defaults(struct omci_device *odev)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_ani_topology topology;
+ u8 data[OMCI_MAX_ATTR_DATA] = {};
+ unsigned int i, queue;
+ int ret;
+
+ ret = omci_agent_get_ani_topology(odev, &topology);
+ if (ret)
+ return ret;
+
+ ret = omci_mib_add_default_mask(agent, OMCI_CLASS_ONU_DATA, 0,
+ BIT(15), data, 1);
+ if (ret)
+ return ret;
+
+ for (i = 0; i < ARRAY_SIZE(agent->config.software_version); i++) {
+ memset(data, 0, sizeof(data));
+ memcpy(data, agent->config.software_version[i],
+       OMCI_SOFTWARE_VERSION_LEN);
+ data[14] = i == 0; /* Committed image. */
+ data[15] = i == 0; /* Active image. */
+ data[16] = 1;      /* Valid image. */
+ ret = omci_mib_add_default_mask(agent, OMCI_CLASS_SOFTWARE_IMAGE,
+ i, GENMASK(15, 12), data,
+ sizeof(data));
+ if (ret)
+ return ret;
+ }
+
+ ret = omci_agent_add_equipment_default(agent, &topology,
+       OMCI_GPON_SLOT, OMCI_GPON_UNIT_TYPE,
+       1, true, data);
+ if (ret)
+ return ret;
+ ret = omci_agent_add_equipment_default(agent, &topology,
+       OMCI_ETHERNET_SLOT,
+       OMCI_ETHERNET_UNIT_TYPE,
+       agent->config.uni_count, false, data);
+ if (ret)
+ return ret;
+ ret = omci_agent_add_equipment_default(agent, &topology,
+       OMCI_VEIP_SLOT, OMCI_VEIP_UNIT_TYPE,
+       1, false, data);
+ if (ret)
+ return ret;
+
+ ret = omci_mib_add_default(agent, OMCI_CLASS_ONU_G, 0, data,
+   sizeof(data));
+ if (ret)
+ return ret;
+ ret = omci_mib_add_default(agent, OMCI_CLASS_ONU2_G, 0, data,
+   sizeof(data));
+ if (ret)
+ return ret;
+ memset(data, 0, sizeof(data));
+ ret = omci_mib_add_default(agent, OMCI_CLASS_ANI_G,
+   OMCI_ANI_G_ENTITY_ID, data,
+   sizeof(data));
+ if (ret)
+ return ret;
+
+ for (i = 0; i < agent->config.uni_count; i++) {
+ u16 entity_id = OMCI_UNI_ENTITY_ID(OMCI_ETHERNET_SLOT, i + 1);
+
+ memset(data, 0, sizeof(data));
+ data[4] = 0; /* Administrative state unlocked. */
+ ret = omci_mib_add_default(agent, OMCI_CLASS_PPTP_ETHERNET_UNI,
+   entity_id, data, sizeof(data));
+ if (ret)
+ return ret;
+
+ omci_uni_g_default_data(data);
+ ret = omci_mib_add_default_mask(agent, OMCI_CLASS_UNI_G,
+ entity_id, GENMASK(15, 12),
+ data, sizeof(data));
+ if (ret)
+ return ret;
+ }
+
+ memset(data, 0, sizeof(data));
+ data[0] = 0; /* Administrative state unlocked. */
+ ret = omci_mib_add_default(agent, OMCI_CLASS_VEIP,
+   OMCI_UNI_ENTITY_ID(OMCI_VEIP_SLOT, 1), data,
+   sizeof(data));
+ if (ret)
+ return ret;
+ omci_uni_g_default_data(data);
+ ret = omci_mib_add_default_mask(agent, OMCI_CLASS_UNI_G,
+ OMCI_UNI_ENTITY_ID(OMCI_VEIP_SLOT, 1),
+ GENMASK(15, 12), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ /* Also seed Slot 6 VEIP (0x0601) for compatibility with
Alcatel-Lucent / EcoNet OLT profiles */
+ memset(data, 0, sizeof(data));
+ data[0] = 0;
+ ret = omci_mib_add_default(agent, OMCI_CLASS_VEIP,
+   OMCI_UNI_ENTITY_ID(6, 1), data,
+   sizeof(data));
+ if (ret)
+ return ret;
+ omci_uni_g_default_data(data);
+ ret = omci_mib_add_default_mask(agent, OMCI_CLASS_UNI_G,
+ OMCI_UNI_ENTITY_ID(6, 1),
+ GENMASK(15, 12), data, sizeof(data));
+ if (ret)
+ return ret;
+
+ for (i = 0; i < topology.tcont_count; i++) {
+ u16 scheduler_id = topology.scheduler_base + i;
+
+ memset(data, 0, sizeof(data));
+ put_unaligned_be16(0xffff, data);
+ ret = omci_mib_add_default(agent, OMCI_CLASS_TCONT,
+   topology.tcont_base + i, data,
+   sizeof(data));
+ if (ret)
+ return ret;
+
+ omci_traffic_scheduler_default_data(&topology, i, data);
+ ret = omci_mib_add_traffic_scheduler_default(agent, scheduler_id, data);
+ if (ret)
+ return ret;
+
+ for (queue = 0; queue < topology.queues_per_tcont; queue++) {
+ u16 entity_id = topology.queue_base +
+ i * topology.queues_per_tcont + queue;
+
+ omci_priority_queue_default_data(&topology, i, queue, data);
+ ret = omci_mib_add_priority_queue_default(agent, entity_id, data);
+ if (ret)
+ return ret;
+ }
+ }
+
+ omci_agent_refresh_identity_locked(agent);
+ return 0;
+}
+
+int omci_agent_init(struct omci_device *odev)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_identity identity;
+ int ret;
+
+ mutex_init(&agent->lock);
+ xa_init(&agent->mib);
+ xa_init(&agent->services);
+ agent->enabled = true;
+ agent->permissive = false;
+ agent->fake_omci = false;
+ agent->dying_gasp = false;
+ agent->config.dying_gasp_source = OMCI_CONFIG_SOURCE_DEFAULT;
+ agent->config.uni_count = 4;
+ agent->config.onu_type = OMCI_ONU_TYPE_HGU;
+ agent->config.onu_type_source = OMCI_CONFIG_SOURCE_DEFAULT;
+ agent->config.traffic_mgmt_option = 0;
+ agent->config.olt_profile = OMCI_OLT_PROFILE_AUTO;
+ agent->config.olt_profile_force = OMCI_OLT_PROFILE_UNSPEC;
+ agent->config.olt_profile_source = OMCI_CONFIG_SOURCE_DEFAULT;
+ agent->config.olt_profile_force_source = OMCI_CONFIG_SOURCE_DEFAULT;
+ memcpy(agent->config.serial_number, "OPEN0001", 8);
+ memcpy(agent->config.vendor_id, "OPEN", 4);
+ strscpy(agent->config.version, "OpenWrt", sizeof(agent->config.version));
+ strscpy(agent->config.software_version[0], "OpenWrt",
+ sizeof(agent->config.software_version[0]));
+ strscpy(agent->config.software_version[1], "OpenWrt",
+ sizeof(agent->config.software_version[1]));
+ agent->config.omcc_version = OMCI_OMCC_VERSION_G988_2010_BASELINE;
+ strscpy(agent->config.equipment_id, "Airoha EN7523",
+ sizeof(agent->config.equipment_id));
+ agent->config.serial_source = OMCI_CONFIG_SOURCE_DEFAULT;
+ agent->config.vendor_source = OMCI_CONFIG_SOURCE_DEFAULT;
+ agent->config.password_source = OMCI_CONFIG_SOURCE_DEFAULT;
+ agent->config.version_source = OMCI_CONFIG_SOURCE_DEFAULT;
+ agent->config.software_version_source[0] = OMCI_CONFIG_SOURCE_DEFAULT;
+ agent->config.software_version_source[1] = OMCI_CONFIG_SOURCE_DEFAULT;
+ agent->config.omcc_version_source = OMCI_CONFIG_SOURCE_DEFAULT;
+ agent->config.equipment_source = OMCI_CONFIG_SOURCE_DEFAULT;
+
+ ret = omci_identity_load(odev->parent, &identity);
+ if (ret)
+ return ret;
+ omci_agent_apply_identity(agent, &identity);
+
+ mutex_lock(&agent->lock);
+ ret = omci_agent_populate_defaults(odev);
+ if (!ret)
+ ret = omci_agent_profile_refresh_locked(odev, NULL, NULL);
+ mutex_unlock(&agent->lock);
+ return ret;
+}
+
+void omci_agent_cleanup(struct omci_device *odev)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_mib_object *object;
+ unsigned long index;
+
+ mutex_lock(&agent->lock);
+ omci_agent_reset_duplicate_locked(agent);
+ omci_agent_reset_table_snapshot_locked(agent);
+ omci_agent_clear_services_locked(odev);
+ xa_for_each(&agent->mib, index, object) {
+ xa_erase(&agent->mib, index);
+ kfree(object);
+ }
+ mutex_unlock(&agent->lock);
+ xa_destroy(&agent->services);
+ xa_destroy(&agent->mib);
+}
+
+static bool omci_mib_object_active(const struct omci_agent *agent,
+   const struct omci_mib_object *object)
+{
+ if (object->pending_delete)
+ return false;
+ if (object->owner_profile &&
+    object->owner_profile != agent->profile_effective)
+ return false;
+
+ return omci_me_active(agent, object->class_id);
+}
+
+static void omci_agent_reset_table_snapshot_locked(struct omci_agent *agent)
+{
+ kfree(agent->table_snapshot);
+ agent->table_snapshot = NULL;
+ agent->table_snapshot_len = 0;
+ agent->table_snapshot_jiffies = 0;
+ agent->table_snapshot_class_id = 0;
+ agent->table_snapshot_entity_id = 0;
+ agent->table_snapshot_mask = 0;
+}
+
+static void
+omci_set_table_snapshot_locked(struct omci_agent *agent, u16 class_id,
+       u16 entity_id, u16 mask, u8 *table,
+       size_t table_len)
+{
+ omci_agent_reset_table_snapshot_locked(agent);
+ agent->table_snapshot = table;
+ agent->table_snapshot_len = table_len;
+ agent->table_snapshot_jiffies = jiffies;
+ agent->table_snapshot_class_id = class_id;
+ agent->table_snapshot_entity_id = entity_id;
+ agent->table_snapshot_mask = mask;
+}
+
+static int omci_agent_build_me_type_table_locked(struct omci_device *odev,
+ u8 **table,
+ size_t *table_len)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_mib_object *object;
+ struct omci_me_class class;
+ unsigned long *classes;
+ unsigned long class_id;
+ unsigned long index;
+ u32 next = 0;
+ size_t count;
+ u8 *data;
+ int ret;
+
+ classes = bitmap_zalloc(U16_MAX + 1, GFP_KERNEL);
+ if (!classes)
+ return -ENOMEM;
+
+ for (;;) {
+ ret = omci_me_class_next(odev, next, &class, &next);
+ if (ret)
+ break;
+ if (class.support >= OMCI_CLASS_SUPPORT_PARSED)
+ set_bit(class.class_id, classes);
+ }
+ if (ret != -ENOENT)
+ goto out_free_bitmap;
+
+ xa_for_each(&agent->mib, index, object) {
+ if (omci_mib_object_active(agent, object))
+ set_bit(object->class_id, classes);
+ }
+ /* Stored bridge/GAL profiles are supported as partial shadow MEs. */
+ set_bit(OMCI_CLASS_MAC_BRIDGE_SERVICE_PROFILE, classes);
+ set_bit(OMCI_CLASS_MAC_BRIDGE_CONFIG_DATA, classes);
+ set_bit(OMCI_CLASS_GAL_ETHERNET_PROFILE, classes);
+ set_bit(OMCI_CLASS_OMCI, classes);
+
+ count = bitmap_weight(classes, U16_MAX + 1);
+ data = kmalloc_array(count, sizeof(u16), GFP_KERNEL);
+ if (!data) {
+ ret = -ENOMEM;
+ goto out_free_bitmap;
+ }
+
+ count = 0;
+ for_each_set_bit(class_id, classes, U16_MAX + 1) {
+ put_unaligned_be16(class_id, data + count * sizeof(u16));
+ count++;
+ }
+ *table = data;
+ *table_len = count * sizeof(u16);
+ ret = 0;
+
+out_free_bitmap:
+ bitmap_free(classes);
+ return ret;
+}
+
+static int omci_agent_build_message_type_table(u8 **table, size_t *table_len)
+{
+ static const u8 message_types[] = {
+ OMCI_MSG_TYPE_CREATE,
+ OMCI_MSG_TYPE_DELETE,
+ OMCI_MSG_TYPE_SET,
+ OMCI_MSG_TYPE_GET,
+ OMCI_MSG_TYPE_GET_ALL_ALARMS,
+ OMCI_MSG_TYPE_GET_ALL_ALARMS_NEXT,
+ OMCI_MSG_TYPE_MIB_UPLOAD,
+ OMCI_MSG_TYPE_MIB_UPLOAD_NEXT,
+ OMCI_MSG_TYPE_MIB_RESET,
+ OMCI_MSG_TYPE_ALARM_NOTIFICATION,
+ OMCI_MSG_TYPE_TEST,
+ OMCI_MSG_TYPE_SYNC_TIME,
+ OMCI_MSG_TYPE_REBOOT,
+ OMCI_MSG_TYPE_GET_NEXT,
+ OMCI_MSG_TYPE_TEST_RESULT,
+ OMCI_MSG_TYPE_GET_CURRENT_DATA,
+ OMCI_MSG_TYPE_SET_TABLE,
+ };
+
+ *table = kmemdup(message_types, sizeof(message_types), GFP_KERNEL);
+ if (!*table)
+ return -ENOMEM;
+ *table_len = sizeof(message_types);
+ return 0;
+}
+
+static u8 omci_agent_get_omci_table_locked(struct omci_device *odev,
+   u16 entity_id, u16 mask,
+   u8 *response_content,
+   size_t response_capacity,
+   size_t *response_len)
+{
+ struct omci_agent *agent = &odev->agent;
+ u8 *table = NULL;
+ size_t table_len = 0;
+ int ret;
+
+ if (entity_id || hweight16(mask) != 1 ||
+    !(mask & (OMCI_OMCI_ME_TYPE_TABLE_MASK |
+      OMCI_OMCI_MESSAGE_TYPE_TABLE_MASK)))
+ return OMCI_RESULT_PARAMETER_ERROR;
+ if (response_capacity < 7)
+ return OMCI_RESULT_PARAMETER_ERROR;
+
+ if (mask == OMCI_OMCI_ME_TYPE_TABLE_MASK)
+ ret = omci_agent_build_me_type_table_locked(odev, &table,
+    &table_len);
+ else
+ ret = omci_agent_build_message_type_table(&table, &table_len);
+ if (ret)
+ return OMCI_RESULT_PROCESSING_ERROR;
+
+ omci_set_table_snapshot_locked(agent, OMCI_CLASS_OMCI, entity_id,
+       mask, table, table_len);
+
+ memset(response_content, 0, response_capacity);
+ response_content[0] = OMCI_RESULT_SUCCESS;
+ put_unaligned_be16(mask, response_content + 1);
+ put_unaligned_be32(table_len, response_content + 3);
+ *response_len = 7;
+ return OMCI_RESULT_SUCCESS;
+}
+
+static bool
+omci_agent_me_type_supported_locked(struct omci_device *odev, u16 class_id)
+{
+ u8 *table;
+ size_t table_len;
+ size_t offset;
+ bool found = false;
+
+ if (omci_agent_build_me_type_table_locked(odev, &table, &table_len))
+ return false;
+ for (offset = 0; offset + sizeof(u16) <= table_len;
+     offset += sizeof(u16)) {
+ if (get_unaligned_be16(table + offset) == class_id) {
+ found = true;
+ break;
+ }
+ }
+ kfree(table);
+
+ return found;
+}
+
+static u8 omci_agent_me_support(const struct omci_me_desc *desc)
+{
+ if (!desc)
+ return 3;
+
+ switch (desc->support) {
+ case OMCI_CLASS_SUPPORT_NATIVE:
+ case OMCI_CLASS_SUPPORT_PROVISIONED:
+ return 1;
+ case OMCI_CLASS_SUPPORT_PARSED:
+ case OMCI_CLASS_SUPPORT_SHADOW:
+ return 3;
+ default:
+ return 2;
+ }
+}
+
+static u8 omci_agent_me_access(const struct omci_me_desc *desc)
+{
+ bool onu_created = desc && (desc->flags & OMCI_ME_F_ONU_CREATED);
+ bool olt_created = desc && (desc->actions & OMCI_ME_ACTION_CREATE);
+
+ if (onu_created && olt_created)
+ return 3;
+ if (olt_created)
+ return 2;
+ if (onu_created)
+ return 1;
+
+ return 3;
+}
+
+static u32 omci_agent_me_actions(const struct omci_me_desc *desc,
+ u16 class_id)
+{
+ if (desc)
+ return desc->actions;
+
+ switch (class_id) {
+ case OMCI_CLASS_MAC_BRIDGE_SERVICE_PROFILE:
+ case OMCI_CLASS_MAC_BRIDGE_CONFIG_DATA:
+ case OMCI_CLASS_GAL_ETHERNET_PROFILE:
+ return OMCI_ME_ACTION_CREATE | OMCI_ME_ACTION_DELETE |
+       OMCI_ME_ACTION_GET | OMCI_ME_ACTION_SET;
+ default:
+ return OMCI_ME_ACTION_GET | OMCI_ME_ACTION_SET;
+ }
+}
+
+static int
+omci_build_attr_ids_locked(struct omci_device *odev, u16 class_id,
+   u8 **table, size_t *table_len)
+{
+ const struct omci_me_desc *desc = omci_me_lookup(&odev->agent, class_id);
+ u8 *data;
+ unsigned int i;
+ int ret;
+
+ if (!desc || !desc->num_attrs) {
+ data = kmalloc(1, GFP_KERNEL);
+ if (!data)
+ return -ENOMEM;
+ *table = data;
+ *table_len = 0;
+ return 0;
+ }
+
+ data = kmalloc_array(desc->num_attrs, sizeof(u16), GFP_KERNEL);
+ if (!data)
+ return -ENOMEM;
+ for (i = 0; i < desc->num_attrs; i++) {
+ u16 attr_id;
+
+ ret = omci_me_attr_id(&odev->agent, class_id, i, &attr_id);
+ if (ret) {
+ kfree(data);
+ return ret;
+ }
+ put_unaligned_be16(attr_id, data + i * sizeof(u16));
+ }
+ *table = data;
+ *table_len = desc->num_attrs * sizeof(u16);
+ return 0;
+}
+
+static int
+omci_build_instances_locked(struct omci_device *odev, u16 class_id,
+    u8 **table, size_t *table_len)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_mib_object *object;
+ unsigned long index;
+ unsigned int count = 0;
+ u8 *data;
+
+ if (class_id == OMCI_CLASS_MANAGED_ENTITY) {
+ return omci_agent_build_me_type_table_locked(odev, table,
+     table_len);
+ }
+ if (class_id == OMCI_CLASS_ATTRIBUTE) {
+ unsigned int attr_count = omci_me_attr_count(agent);
+ unsigned int i;
+
+ data = kmalloc_array(max(attr_count, 1U), sizeof(u16),
+     GFP_KERNEL);
+ if (!data)
+ return -ENOMEM;
+ for (i = 0; i < attr_count; i++)
+ put_unaligned_be16(i + 1, data + i * sizeof(u16));
+ *table = data;
+ *table_len = attr_count * sizeof(u16);
+ return 0;
+ }
+ if (class_id == OMCI_CLASS_OMCI) {
+ data = kmalloc(sizeof(u16), GFP_KERNEL);
+ if (!data)
+ return -ENOMEM;
+ put_unaligned_be16(0, data);
+ *table = data;
+ *table_len = sizeof(u16);
+ return 0;
+ }
+
+ xa_for_each(&agent->mib, index, object)
+ if (object->class_id == class_id &&
+    omci_mib_object_active(agent, object))
+ count++;
+ data = kmalloc_array(max(count, 1U), sizeof(u16), GFP_KERNEL);
+ if (!data)
+ return -ENOMEM;
+ count = 0;
+ xa_for_each(&agent->mib, index, object) {
+ if (object->class_id != class_id ||
+    !omci_mib_object_active(agent, object))
+ continue;
+ put_unaligned_be16(object->entity_id,
+   data + count * sizeof(u16));
+ count++;
+ }
+ *table = data;
+ *table_len = count * sizeof(u16);
+ return 0;
+}
+
+static int omci_agent_empty_table(u8 **table, size_t *table_len)
+{
+ *table = kmalloc(1, GFP_KERNEL);
+ if (!*table)
+ return -ENOMEM;
+ *table_len = 0;
+ return 0;
+}
+
+static u8 omci_agent_get_managed_entity_locked(struct omci_device *odev,
+       u16 entity_id, u16 mask,
+       u8 *response_content,
+       size_t response_capacity,
+       size_t *response_len)
+{
+ struct omci_agent *agent = &odev->agent;
+ const struct omci_me_desc *desc;
+ size_t used = 3;
+ u16 encoded = 0;
+ u8 *table = NULL;
+ size_t table_len = 0;
+ unsigned int bit;
+ int ret;
+
+ if (!mask || mask & ~GENMASK(15, 8))
+ return OMCI_RESULT_PARAMETER_ERROR;
+ if (!omci_agent_me_type_supported_locked(odev, entity_id))
+ return OMCI_RESULT_UNKNOWN_INSTANCE;
+ desc = omci_me_lookup(agent, entity_id);
+
+ if (mask & OMCI_MANAGED_ENTITY_TABLE_MASKS) {
+ if (hweight16(mask) != 1 || response_capacity < 7)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ switch (mask) {
+ case OMCI_MANAGED_ENTITY_ATTR_TABLE_MASK:
+ ret = omci_build_attr_ids_locked(odev, entity_id, &table,
+ &table_len);
+ break;
+ case OMCI_MANAGED_ENTITY_INSTANCES_MASK:
+ ret = omci_build_instances_locked(odev, entity_id,
+  &table, &table_len);
+ break;
+ default:
+ ret = omci_agent_empty_table(&table, &table_len);
+ break;
+ }
+ if (ret)
+ return OMCI_RESULT_PROCESSING_ERROR;
+ omci_set_table_snapshot_locked(agent, OMCI_CLASS_MANAGED_ENTITY,
+       entity_id, mask, table, table_len);
+ memset(response_content, 0, response_capacity);
+ response_content[0] = OMCI_RESULT_SUCCESS;
+ put_unaligned_be16(mask, response_content + 1);
+ put_unaligned_be32(table_len, response_content + 3);
+ *response_len = 7;
+ return OMCI_RESULT_SUCCESS;
+ }
+
+ memset(response_content, 0, response_capacity);
+ response_content[0] = OMCI_RESULT_SUCCESS;
+ for (bit = 0; bit < 16; bit++) {
+ u16 attr_mask = BIT(15 - bit);
+
+ if (!(mask & attr_mask))
+ continue;
+ switch (attr_mask) {
+ case OMCI_MANAGED_ENTITY_NAME_MASK: {
+ const char *name = omci_me_class_name(entity_id);
+ size_t len = strnlen(name, OMCI_CAPABILITY_NAME_LEN);
+
+ if (used + OMCI_CAPABILITY_NAME_LEN > response_capacity)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ memcpy(response_content + used, name, len);
+ used += OMCI_CAPABILITY_NAME_LEN;
+ break;
+ }
+ case OMCI_MANAGED_ENTITY_ACCESS_MASK:
+ if (used + 1 > response_capacity)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ response_content[used++] = omci_agent_me_access(desc);
+ break;
+ case OMCI_MANAGED_ENTITY_ACTIONS_MASK:
+ if (used + 4 > response_capacity)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ put_unaligned_be32(omci_agent_me_actions(desc, entity_id),
+   response_content + used);
+ used += 4;
+ break;
+ case OMCI_MANAGED_ENTITY_SUPPORT_MASK:
+ if (used + 1 > response_capacity)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ response_content[used++] = omci_agent_me_support(desc);
+ break;
+ default:
+ return OMCI_RESULT_PARAMETER_ERROR;
+ }
+ encoded |= attr_mask;
+ }
+ put_unaligned_be16(encoded, response_content + 1);
+ *response_len = used;
+ return OMCI_RESULT_SUCCESS;
+}
+
+static bool omci_agent_attr_is_table(u16 class_id, u16 mask)
+{
+ if (class_id == OMCI_CLASS_OMCI)
+ return mask & (OMCI_OMCI_ME_TYPE_TABLE_MASK |
+       OMCI_OMCI_MESSAGE_TYPE_TABLE_MASK);
+ if (class_id == OMCI_CLASS_MANAGED_ENTITY)
+ return mask & OMCI_MANAGED_ENTITY_TABLE_MASKS;
+ if (class_id == OMCI_CLASS_ATTRIBUTE)
+ return mask & OMCI_ATTRIBUTE_CODE_POINTS_MASK;
+ if (class_id == OMCI_CLASS_EXTENDED_VLAN)
+ return mask == BIT(8);
+
+ return false;
+}
+
+static u8 omci_agent_attr_format(u16 class_id,
+ const struct omci_attr_desc *attr)
+{
+ if (omci_agent_attr_is_table(class_id, attr->mask))
+ return OMCI_ATTRIBUTE_FORMAT_TABLE;
+ if (attr->len > 4)
+ return OMCI_ATTRIBUTE_FORMAT_STRING;
+ if (attr->len == 4 && attr->mask == GENMASK(15, 0))
+ return OMCI_ATTRIBUTE_FORMAT_BIT_FIELD;
+
+ return OMCI_ATTRIBUTE_FORMAT_UNSIGNED;
+}
+
+static u8 omci_agent_get_attribute_locked(struct omci_device *odev,
+  u16 entity_id, u16 mask,
+  u8 *response_content,
+  size_t response_capacity,
+  size_t *response_len)
+{
+ struct omci_agent *agent = &odev->agent;
+ const struct omci_me_desc *desc;
+ const struct omci_attr_desc *attr;
+ unsigned int attr_index;
+ size_t used = 3;
+ u16 class_id;
+ u16 encoded = 0;
+ unsigned int bit;
+ u32 upper;
+ int ret;
+
+ if (!mask || mask & ~GENMASK(15, 7))
+ return OMCI_RESULT_PARAMETER_ERROR;
+ ret = omci_me_attr_get(agent, entity_id, &class_id, &attr_index,
+       &desc, &attr);
+ if (ret)
+ return OMCI_RESULT_UNKNOWN_INSTANCE;
+
+ if (mask & OMCI_ATTRIBUTE_TABLE_MASKS) {
+ u8 *table;
+ size_t table_len;
+
+ if (hweight16(mask) != 1 || response_capacity < 7)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ ret = omci_agent_empty_table(&table, &table_len);
+ if (ret)
+ return OMCI_RESULT_PROCESSING_ERROR;
+ omci_set_table_snapshot_locked(agent, OMCI_CLASS_ATTRIBUTE,
+       entity_id, mask, table, table_len);
+ memset(response_content, 0, response_capacity);
+ response_content[0] = OMCI_RESULT_SUCCESS;
+ put_unaligned_be16(mask, response_content + 1);
+ put_unaligned_be32(table_len, response_content + 3);
+ *response_len = 7;
+ return OMCI_RESULT_SUCCESS;
+ }
+
+ memset(response_content, 0, response_capacity);
+ response_content[0] = OMCI_RESULT_SUCCESS;
+ for (bit = 0; bit < 16; bit++) {
+ u16 attr_mask = BIT(15 - bit);
+
+ if (!(mask & attr_mask))
+ continue;
+ switch (attr_mask) {
+ case OMCI_ATTRIBUTE_NAME_MASK: {
+ char name[OMCI_CAPABILITY_NAME_LEN + 1];
+
+ if (used + OMCI_CAPABILITY_NAME_LEN > response_capacity)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ snprintf(name, sizeof(name), "%u attribute %u",
+ class_id, attr_index + 1);
+ memcpy(response_content + used, name,
+       strnlen(name, OMCI_CAPABILITY_NAME_LEN));
+ used += OMCI_CAPABILITY_NAME_LEN;
+ break;
+ }
+ case OMCI_ATTRIBUTE_SIZE_MASK:
+ if (used + 2 > response_capacity)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ put_unaligned_be16(attr->len, response_content + used);
+ used += 2;
+ break;
+ case OMCI_ATTRIBUTE_ACCESS_MASK:
+ if (used + 1 > response_capacity)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ response_content[used++] = attr->access & 0x7;
+ break;
+ case OMCI_ATTRIBUTE_FORMAT_MASK:
+ if (used + 1 > response_capacity)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ response_content[used++] =
+ omci_agent_attr_format(class_id, attr);
+ break;
+ case OMCI_ATTRIBUTE_LOWER_LIMIT_MASK:
+ case OMCI_ATTRIBUTE_BIT_FIELD_MASK:
+ if (used + 4 > response_capacity)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ put_unaligned_be32(0, response_content + used);
+ used += 4;
+ break;
+ case OMCI_ATTRIBUTE_UPPER_LIMIT_MASK:
+ if (used + 4 > response_capacity)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ upper = attr->len >= sizeof(u32) ? U32_MAX :
+ GENMASK(attr->len * 8 - 1, 0);
+ put_unaligned_be32(upper, response_content + used);
+ used += 4;
+ break;
+ case OMCI_ATTRIBUTE_SUPPORT_MASK:
+ if (used + 1 > response_capacity)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ response_content[used++] = 3;
+ break;
+ default:
+ return OMCI_RESULT_PARAMETER_ERROR;
+ }
+ encoded |= attr_mask;
+ }
+ put_unaligned_be16(encoded, response_content + 1);
+ *response_len = used;
+ return OMCI_RESULT_SUCCESS;
+}
+
+static u8 omci_agent_get_next_locked(struct omci_device *odev,
+     u16 class_id, u16 entity_id,
+     u8 device_id, const u8 *content,
+     size_t content_len, u8 *response_content,
+     size_t response_capacity,
+     size_t *response_len)
+{
+ struct omci_agent *agent = &odev->agent;
+ size_t fragment_len;
+ size_t copy_len;
+ size_t offset;
+ u16 sequence;
+ u16 mask;
+
+ if (content_len < 4 || response_capacity < 3)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ if (!omci_me_active(agent, class_id))
+ return OMCI_RESULT_UNKNOWN_ME;
+ if (!omci_me_action_allowed(agent, class_id, OMCI_MSG_TYPE_GET_NEXT))
+ return OMCI_RESULT_NOT_SUPPORTED;
+ mask = get_unaligned_be16(content);
+ sequence = get_unaligned_be16(content + 2);
+ if (hweight16(mask) != 1)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ if (!agent->table_snapshot ||
+    time_after(jiffies, agent->table_snapshot_jiffies +
+       OMCI_TABLE_SNAPSHOT_TIMEOUT) ||
+    agent->table_snapshot_class_id != class_id ||
+    agent->table_snapshot_entity_id != entity_id ||
+    agent->table_snapshot_mask != mask)
+ return OMCI_RESULT_PARAMETER_ERROR;
+
+ fragment_len = device_id == OMCI_BASELINE_DEV_ID ?
+ OMCI_BASELINE_TABLE_FRAGMENT_LEN : response_capacity - 3;
+ if (sequence > SIZE_MAX / fragment_len)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ offset = (size_t)sequence * fragment_len;
+ if (offset >= agent->table_snapshot_len)
+ return OMCI_RESULT_PARAMETER_ERROR;
+
+ copy_len = min(fragment_len, agent->table_snapshot_len - offset);
+ memset(response_content, 0, response_capacity);
+ response_content[0] = OMCI_RESULT_SUCCESS;
+ put_unaligned_be16(mask, response_content + 1);
+ memcpy(response_content + 3, agent->table_snapshot + offset, copy_len);
+ *response_len = 3 + copy_len;
+ agent->table_snapshot_jiffies = jiffies;
+ return OMCI_RESULT_SUCCESS;
+}
+
+static bool omci_mib_object_uploadable(const struct omci_agent *agent,
+       const struct omci_mib_object *object)
+{
+ const struct omci_me_desc *desc;
+
+ if (!omci_mib_object_active(agent, object))
+ return false;
+ desc = omci_me_lookup(agent, object->class_id);
+
+ return !desc || !(desc->flags & OMCI_ME_F_NO_MIB_UPLOAD);
+}
+
+static unsigned int omci_mib_count_locked(struct omci_agent *agent)
+{
+ struct omci_mib_object *object;
+ unsigned long index;
+ unsigned int count = 0;
+
+ xa_for_each(&agent->mib, index, object)
+ if (omci_mib_object_uploadable(agent, object))
+ count++;
+ return count;
+}
+
+static void omci_agent_reset_olt_objects_locked(struct omci_agent *agent)
+{
+ struct omci_mib_object *object;
+ unsigned long index;
+
+ xa_for_each(&agent->mib, index, object) {
+ if (object->origin != OMCI_MIB_ORIGIN_OLT)
+ continue;
+ xa_erase(&agent->mib, index);
+ kfree(object);
+ }
+}
+
+static int omci_agent_mib_reset_locked(struct omci_device *odev, bool all,
+       bool *profile_changed)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_mib_object *object;
+ unsigned long index;
+ int reset_ret;
+ int ret;
+
+ omci_agent_reset_table_snapshot_locked(agent);
+ reset_ret = omci_agent_clear_services_locked(odev);
+
+ if (all) {
+ xa_for_each(&agent->mib, index, object) {
+ xa_erase(&agent->mib, index);
+ kfree(object);
+ }
+ } else {
+ omci_agent_reset_olt_objects_locked(agent);
+ }
+
+ /*
+ * A MIB reset restores ONU-created MEs to their initial values. Rebuild
+ * them as well as the selected profile's vendor MEs so repeated resets
+ * expose the same MIB and capability table as a fresh agent.
+ */
+ ret = omci_agent_populate_defaults(odev);
+ if (!ret)
+ ret = omci_agent_seed_profile_locked(odev,
+     agent->profile_effective);
+ if (!ret)
+ ret = omci_agent_profile_refresh_locked(odev, NULL, profile_changed);
+
+ omci_agent_set_mib_sync_locked(agent, 0);
+ agent->upload_index = 0;
+ omci_agent_reset_duplicate_locked(agent);
+
+ /*
+ * omci_agent_clear_services_locked() above tore down every applied
+ * service. The MIB objects it left in place (or just rebuilt) may
+ * still describe a complete datapath - e.g. a profile's default-
+ * seeded VEIP bridge, which a MIB Reset never touches since it only
+ * erases OLT-origin objects. Without this, a MIB Reset (which every
+ * OLT sends as a normal part of post-activation provisioning)
+ * leaves the ONU in O5 with OMCI up and no datapath, indefinitely,
+ * until some unrelated event happens to trigger reconcile again.
+ */
+ if (!ret)
+ omci_agent_reconcile_services_locked(odev);
+
+ return ret ?: reset_ret;
+}
+
+static int omci_agent_hw_update(struct omci_device *odev,
+ struct omci_mib_object *object,
+ u8 action, const u8 *content)
+{
+ const struct omci_device_ops *ops = odev->ops;
+ const struct omci_me_desc *desc;
+ bool ignore_unsupported_uni;
+ bool enable;
+ u16 entity_id;
+ u16 value;
+ int ret;
+
+ desc = omci_me_lookup(&odev->agent, object->class_id);
+ if (action != OMCI_MSG_TYPE_DELETE && desc &&
+    (desc->flags & OMCI_ME_F_DATAPATH) &&
+    desc->support == OMCI_CLASS_SUPPORT_SHADOW)
+ return -EOPNOTSUPP;
+
+ switch (object->class_id) {
+ case OMCI_CLASS_TCONT:
+ if (!ops->set_tcont)
+ return 0;
+ if (action == OMCI_MSG_TYPE_DELETE)
+ return ops->set_tcont(odev, object->entity_id, 0xffff,
+      false);
+ value = get_unaligned_be16(object->data);
+ return ops->set_tcont(odev, object->entity_id, value, true);
+ case OMCI_CLASS_GEM_PORT_CTP:
+ if (!ops->set_gem_port)
+ return 0;
+ return ops->set_gem_port(odev, object->entity_id,
+ get_unaligned_be16(object->data),
+ get_unaligned_be16(object->data + 2),
+ object->data[4],
+ action != OMCI_MSG_TYPE_DELETE, false);
+ case OMCI_CLASS_PPTP_ETHERNET_UNI:
+ case OMCI_CLASS_VEIP:
+ if (!ops->set_uni)
+ return 0;
+ if (action == OMCI_MSG_TYPE_DELETE)
+ enable = false;
+ else if (object->class_id == OMCI_CLASS_PPTP_ETHERNET_UNI)
+ enable = object->data[4] == 0;
+ else
+ enable = object->data[0] == 0;
+ entity_id = omci_profile_normalize_uni_entity(
+ odev->agent.profile_effective, object->entity_id);
+ ret = ops->set_uni(odev, entity_id, enable);
+ ignore_unsupported_uni = odev->agent.profile_quirks &
+ OMCI_OLT_QUIRK_IGNORE_UNSUPPORTED_UNI;
+ if (ret == -EOPNOTSUPP && ignore_unsupported_uni)
+ return 0;
+ return ret;
+ default:
+ return 0;
+ }
+}
+
+static bool
+omci_agent_datapath_class(const struct omci_agent *agent, u16 class_id)
+{
+ const struct omci_me_desc *desc = omci_me_lookup(agent, class_id);
+
+ return desc && (desc->flags & OMCI_ME_F_DATAPATH);
+}
+
+static void omci_agent_free_service_array(struct xarray *services)
+{
+ struct omci_service_state *state;
+ unsigned long index;
+
+ xa_for_each(services, index, state) {
+ xa_erase(services, index);
+ kfree(state);
+ }
+}
+
+static int omci_agent_clear_services_locked(struct omci_device *odev)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_service_state *state;
+ unsigned long index;
+ int first_error = 0;
+ int ret;
+
+ xa_for_each(&agent->services, index, state) {
+ if (odev->ops->delete_service) {
+ ret = odev->ops->delete_service(odev, state->config.cookie);
+ if (ret && ret != -ENOENT && !first_error)
+ first_error = ret;
+ }
+ xa_erase(&agent->services, index);
+ kfree(state);
+ }
+
+ return first_error;
+}
+
+static int omci_agent_stage_service(struct xarray *services,
+    const struct omci_service_config *service)
+{
+ struct omci_service_state *state;
+ void *old;
+
+ state = kmalloc(sizeof(*state), GFP_KERNEL);
+ if (!state)
+ return -ENOMEM;
+ state->config = *service;
+
+ old = xa_store(services, service->cookie, state, GFP_KERNEL);
+ if (xa_is_err(old)) {
+ kfree(state);
+ return xa_err(old);
+ }
+ kfree(old);
+ return 0;
+}
+
+static int omci_agent_apply_services_locked(struct omci_device *odev,
+    struct xarray *desired)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_service_state *state;
+ struct omci_service_state *old_state;
+ unsigned long index;
+ int ret = 0;
+
+ if (!xa_empty(desired) && !odev->ops->replace_service)
+ return -EOPNOTSUPP;
+
+ /* Reserve all new cookies before changing hardware state. */
+ xa_for_each(desired, index, state) {
+ if (xa_load(&agent->services, index))
+ continue;
+ ret = xa_reserve(&agent->services, index, GFP_KERNEL);
+ if (ret)
+ goto release_reservations;
+ }
+
+ /* Add or replace all desired rules while the previous set remains live. */
+ xa_for_each(desired, index, state) {
+ ret = odev->ops->replace_service(odev, &state->config);
+ if (ret) {
+ dev_warn(odev->parent,
+ "OMCI install failed for service %#x (UNI %#x GEM %u T-CONT %#x
queue %u): %d; rolling back\n",
+ state->config.cookie,
+ state->config.uni_entity_id,
+ state->config.gem_port_id,
+ state->config.tcont_entity_id,
+ state->config.queue, ret);
+ goto rollback_hardware;
+ }
+ }
+
+ /* Delete rules that are no longer part of the resolved service graph. */
+ xa_for_each(&agent->services, index, old_state) {
+ if (xa_load(desired, index))
+ continue;
+ if (!odev->ops->delete_service)
+ continue;
+ ret = odev->ops->delete_service(odev, old_state->config.cookie);
+ if (ret && ret != -ENOENT)
+ goto rollback_hardware;
+ }
+
+ /* Hardware is consistent; replace the software snapshot. */
+ omci_agent_free_service_array(&agent->services);
+ xa_for_each(desired, index, state) {
+ void *entry;
+
+ xa_erase(desired, index);
+ entry = xa_store(&agent->services, index, state, GFP_KERNEL);
+ if (WARN_ON(xa_is_err(entry))) {
+ kfree(state);
+ continue;
+ }
+ kfree(entry);
+ }
+ return 0;
+
+rollback_hardware:
+ /* Restore every old rule, including rules deleted before a failure. */
+ if (odev->ops->replace_service)
+ xa_for_each(&agent->services, index, old_state)
+ odev->ops->replace_service(odev, &old_state->config);
+
+ /* Remove newly introduced cookies that were not in the old snapshot. */
+ if (odev->ops->delete_service)
+ xa_for_each(desired, index, state)
+ if (!xa_load(&agent->services, index))
+ odev->ops->delete_service(odev,
+  state->config.cookie);
+
+release_reservations:
+ xa_for_each(desired, index, state)
+ if (!xa_load(&agent->services, index))
+ xa_release(&agent->services, index);
+ return ret;
+}
+
+static bool omci_agent_uni_enabled(struct omci_agent *agent, u8 tp_type,
+   u16 entity_id)
+{
+ struct omci_mib_object *object;
+
+ if (tp_type == OMCI_BRIDGE_TP_PPTP_ETH_UNI) {
+ object = omci_mib_lookup(agent, OMCI_CLASS_PPTP_ETHERNET_UNI,
+ entity_id);
+ return object && object->data[4] == 0;
+ }
+ if (tp_type == OMCI_BRIDGE_TP_VEIP) {
+ object = omci_mib_lookup(agent, OMCI_CLASS_VEIP, entity_id);
+ return object && object->data[0] == 0;
+ }
+
+ return false;
+}
+
+static u32 omci_agent_service_cookie(u16 lan_port, u16 gem_ctp,
+     u16 gem_port, u16 selector)
+{
+ u32 cookie;
+
+ cookie = jhash_3words(((u32)lan_port << 16) | gem_ctp,
+      ((u32)gem_port << 16) | selector,
+      0x4f4d4349, 0);
+
+ return cookie ?: 1;
+}
+
+static bool omci_agent_service_vlan_valid(u16 vid)
+{
+ return vid > 0 && vid < VLAN_VID_MASK;
+}
+
+static int
+omci_agent_stage_service_rule_locked(struct omci_device *odev,
+     struct xarray *services,
+     u16 lan_port_entity,
+     u16 uni_entity, u16 gem_iwtp_entity,
+     u8 mapper_pcp, bool mapper_pcp_valid,
+     const struct omci_vlan_filter_entry *filter,
+     const struct omci_extended_vlan_rule *ext_rule,
+     u16 selector, bool *default_installed,
+     bool multicast, u16 ani_entity_id,
+     bool ani_valid)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_mib_object *iwtp;
+ struct omci_mib_object *gem;
+ struct omci_mib_object *tcont;
+ struct omci_service_config service = {};
+ u16 gem_ctp_entity;
+
+ iwtp = omci_mib_lookup(agent, OMCI_CLASS_GEM_IWTP, gem_iwtp_entity);
+ if (!iwtp)
+ return -ENOENT;
+ gem_ctp_entity = get_unaligned_be16(iwtp->data);
+ gem = omci_mib_lookup(agent, OMCI_CLASS_GEM_PORT_CTP, gem_ctp_entity);
+ if (!gem)
+ return -ENOENT;
+ if (gem->data[4] == OMCI_GEM_PORT_DIRECTION_ANI_TO_UNI)
+ return 0;
+
+ service.uni_entity_id = omci_profile_normalize_uni_entity(
+ agent->profile_effective, uni_entity);
+ service.gem_ctp_entity_id = gem_ctp_entity;
+ service.gem_port_id = get_unaligned_be16(gem->data);
+ service.tcont_entity_id = get_unaligned_be16(gem->data + 2);
+ tcont = omci_mib_lookup(agent, OMCI_CLASS_TCONT,
+ service.tcont_entity_id);
+ if (!tcont || get_unaligned_be16(tcont->data) == 0xffff)
+ return -ENOENT;
+ /* Carry the Alloc-ID: the backend may need it to rebuild the channel. */
+ service.alloc_id = get_unaligned_be16(tcont->data);
+ service.direction = gem->data[4];
+ service.multicast = multicast;
+ service.multicast_ani_entity_id = ani_entity_id;
+ service.multicast_ani_valid = ani_valid;
+ if (mapper_pcp_valid) {
+ service.pcp = mapper_pcp;
+ service.pcp_valid = true;
+ service.queue = mapper_pcp;
+ }
+ if (filter) {
+ service.vlan_id = filter->vid;
+ service.vlan_valid =
+ omci_agent_service_vlan_valid(filter->vid);
+ service.pcp = filter->pbit;
+ service.pcp_valid = true;
+ service.queue = filter->pbit;
+ }
+ if (ext_rule) {
+ struct omci_extended_vlan_rule rule = *ext_rule;
+ bool network_vlan = false;
+
+ omci_profile_normalize_vlan_rule(agent->profile_effective, &rule);
+ memcpy(service.vlan_treatment, rule.raw + 8,
+       sizeof(service.vlan_treatment));
+
+ /*
+ * pon0 exposes the ANI/network side of the service.  Class 171
+ * therefore selects the VLAN after the OMCI treatment, not the
+ * customer-side filter VLAN.  Userspace creates that network VLAN
+ * directly (for example pon0.1894), so the backend only needs the
+ * resulting classifier and does not need to rewrite the skb.
+ */
+ if (omci_agent_service_vlan_valid(rule.treat_inner_vid)) {
+ service.vlan_id = rule.treat_inner_vid;
+ network_vlan = true;
+ } else if (omci_agent_service_vlan_valid(rule.treat_outer_vid)) {
+ service.vlan_id = rule.treat_outer_vid;
+ network_vlan = true;
+ } else if (omci_agent_service_vlan_valid(rule.filter_inner_vid)) {
+ service.vlan_id = rule.filter_inner_vid;
+ network_vlan = true;
+ } else if (omci_agent_service_vlan_valid(rule.filter_outer_vid)) {
+ service.vlan_id = rule.filter_outer_vid;
+ network_vlan = true;
+ }
+ service.vlan_valid = network_vlan;
+
+ if (rule.filter_inner_pbit <= 7) {
+ service.pcp = rule.filter_inner_pbit;
+ service.pcp_valid = true;
+ service.queue = rule.filter_inner_pbit;
+ } else if (rule.filter_outer_pbit <= 7) {
+ service.pcp = rule.filter_outer_pbit;
+ service.pcp_valid = true;
+ service.queue = rule.filter_outer_pbit;
+ }
+
+ /*
+ * Preparatory Class 171 rules can describe only a customer-side
+ * rewrite and still carry no host-facing VLAN.  They are valid OMCI
+ * state, but they must not make the whole mapper Set fail.
+ */
+ if (!network_vlan)
+ return -EOPNOTSUPP;
+ }
+ if (!service.vlan_valid && !service.pcp_valid && !*default_installed) {
+ service.default_service = true;
+ *default_installed = true;
+ }
+ service.cookie = omci_agent_service_cookie(lan_port_entity,
+ gem_ctp_entity, service.gem_port_id, selector);
+
+ /*
+ * Trace every rule the resolver wants, not only the ones the backend
+ * accepts, so an install failure can be attributed to a specific rule.
+ */
+ dev_dbg(odev->parent,
+ "OMCI stage service %#x: UNI %#x (raw %#x) LAN port %#x GEM %u
T-CONT %#x queue %u PCP %s%u VLAN %s%u selector %u%s%s\n",
+ service.cookie, service.uni_entity_id, uni_entity,
+ lan_port_entity, service.gem_port_id, service.tcont_entity_id,
+ service.queue, service.pcp_valid ? "" : "any/", service.pcp,
+ service.vlan_valid ? "" : "any/", service.vlan_id, selector,
+ service.multicast ? " multicast" : "",
+ service.default_service ? " default" : "");
+
+ return omci_agent_stage_service(services, &service);
+}
+
+static bool
+omci_agent_vlan_associated(const struct omci_mib_object *object,
+   u16 lan_port_entity, u16 uni_entity,
+   u16 bridge_port_entity, u16 service_entity)
+{
+ if (object->class_id == OMCI_CLASS_VLAN_TAGGING_FILTER_DATA)
+ return object->entity_id == lan_port_entity ||
+       object->entity_id == uni_entity ||
+       object->entity_id == bridge_port_entity ||
+       object->entity_id == service_entity;
+ if (object->class_id == OMCI_CLASS_EXTENDED_VLAN)
+ return object->extended_vlan.valid &&
+       (object->extended_vlan.associated_me == lan_port_entity ||
+ object->extended_vlan.associated_me == uni_entity ||
+ object->extended_vlan.associated_me == bridge_port_entity ||
+ object->extended_vlan.associated_me == service_entity ||
+ object->entity_id == lan_port_entity ||
+ object->entity_id == bridge_port_entity ||
+ object->entity_id == service_entity);
+
+ return false;
+}
+
+static int
+omci_agent_stage_path_locked(struct omci_device *odev,
+     struct xarray *services,
+     const struct omci_mib_object *lan_port,
+     u16 uni_entity, u16 bridge_port_entity,
+     u16 service_entity, u16 gem_iwtp_entity,
+     u8 pcp, bool pcp_valid, bool multicast,
+     u16 ani_entity_id, bool ani_valid,
+     bool *default_installed)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_mib_object *object;
+ unsigned long index;
+ u16 selector = pcp_valid ? 0x100 | pcp : 0;
+ bool installed = false;
+ int ret;
+
+ xa_for_each(&agent->mib, index, object) {
+ unsigned int i;
+
+ if (!omci_mib_object_active(agent, object) ||
+    !omci_agent_vlan_associated(object, lan_port->entity_id,
+ uni_entity, bridge_port_entity,
+ service_entity))
+ continue;
+ if (object->class_id == OMCI_CLASS_VLAN_TAGGING_FILTER_DATA &&
+    object->vlan_filter.valid) {
+ for (i = 0; i < object->vlan_filter.num_entries; i++) {
+ ret = omci_agent_stage_service_rule_locked(
+ odev, services, lan_port->entity_id,
+ uni_entity, gem_iwtp_entity, pcp, pcp_valid,
+ &object->vlan_filter.entries[i], NULL,
+ selector + i + 1, default_installed,
+ multicast, ani_entity_id, ani_valid);
+ if (ret == -EOPNOTSUPP)
+ continue;
+ if (ret)
+ return ret;
+ installed = true;
+ }
+ } else if (object->class_id == OMCI_CLASS_EXTENDED_VLAN &&
+   object->extended_vlan.valid) {
+ for (i = 0; i < object->extended_vlan.rule_count; i++) {
+ ret = omci_agent_stage_service_rule_locked(
+ odev, services, lan_port->entity_id,
+ uni_entity, gem_iwtp_entity, pcp, pcp_valid,
+ NULL, &object->extended_vlan.rules[i],
+ selector + i + 0x20, default_installed,
+ multicast, ani_entity_id, ani_valid);
+ if (ret == -EOPNOTSUPP)
+ continue;
+ if (ret)
+ return ret;
+ installed = true;
+ }
+ }
+ }
+
+ if (installed)
+ return 0;
+
+ return omci_agent_stage_service_rule_locked(
+ odev, services, lan_port->entity_id, uni_entity, gem_iwtp_entity,
+ pcp, pcp_valid, NULL, NULL, selector, default_installed,
+ multicast, ani_entity_id, ani_valid);
+}
+
+static int
+omci_agent_resolve_bridge_locked(struct omci_device *odev,
+ struct xarray *services,
+ const struct omci_mib_object *lan_port,
+ bool *default_installed)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_mib_object *wan_port;
+ struct omci_mib_object *mapper;
+ unsigned long index;
+ u16 bridge_id = get_unaligned_be16(lan_port->data);
+ u16 uni_entity = get_unaligned_be16(lan_port->data + 4);
+ u8 lan_type = lan_port->data[3];
+ int ret;
+
+ if (!omci_agent_uni_enabled(agent, lan_type, uni_entity))
+ return 0;
+
+ xa_for_each(&agent->mib, index, wan_port) {
+ u16 mapper_entity;
+ u16 gem_iwtp_entity;
+ u8 wan_type;
+ unsigned int pcp;
+
+ if (wan_port->class_id !=
+    OMCI_CLASS_MAC_BRIDGE_PORT_CONFIG_DATA ||
+    wan_port == lan_port ||
+    get_unaligned_be16(wan_port->data) != bridge_id)
+ continue;
+ wan_type = wan_port->data[3];
+ if (wan_type == OMCI_BRIDGE_TP_GEM_IWTP ||
+    wan_type == OMCI_BRIDGE_TP_MULTICAST_GEM_IWTP) {
+ bool multicast =
+ wan_type == OMCI_BRIDGE_TP_MULTICAST_GEM_IWTP;
+ bool ani_valid = false;
+ u16 ani_entity_id = 0;
+
+ gem_iwtp_entity = get_unaligned_be16(wan_port->data + 4);
+ if (multicast &&
+    !omci_profile_resolve_multicast_ani(
+ agent->profile_effective, wan_port->entity_id,
+ &ani_entity_id))
+ ani_valid = true;
+ ret = omci_agent_stage_path_locked(
+ odev, services, lan_port, uni_entity,
+ wan_port->entity_id, gem_iwtp_entity,
+ gem_iwtp_entity, 0, false, multicast,
+ ani_entity_id, ani_valid, default_installed);
+ if (ret && ret != -ENOENT)
+ return ret;
+ continue;
+ }
+ if (wan_type != OMCI_BRIDGE_TP_8021P_MAPPER)
+ continue;
+ mapper_entity = get_unaligned_be16(wan_port->data + 4);
+ mapper = omci_mib_lookup(agent, OMCI_CLASS_8021P_MAPPER,
+ mapper_entity);
+ if (!mapper)
+ continue;
+ for (pcp = 0; pcp < 8; pcp++) {
+ gem_iwtp_entity = get_unaligned_be16(mapper->data + 2 +
+ pcp * 2);
+ if (!gem_iwtp_entity || gem_iwtp_entity == 0xffff)
+ continue;
+ ret = omci_agent_stage_path_locked(
+ odev, services, lan_port, uni_entity,
+ wan_port->entity_id, mapper_entity,
+ gem_iwtp_entity, pcp, true, false, 0, false,
+ default_installed);
+ if (ret && ret != -ENOENT)
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+static int omci_agent_reconcile_services_locked(struct omci_device *odev)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_mib_object *object;
+ struct xarray desired;
+ unsigned long index;
+ bool default_installed = false;
+ int ret;
+
+ xa_init(&desired);
+ xa_for_each(&agent->mib, index, object) {
+ u8 tp_type;
+
+ if (!omci_mib_object_active(agent, object) ||
+    object->class_id != OMCI_CLASS_MAC_BRIDGE_PORT_CONFIG_DATA)
+ continue;
+ tp_type = object->data[3];
+ if (tp_type != OMCI_BRIDGE_TP_PPTP_ETH_UNI &&
+    tp_type != OMCI_BRIDGE_TP_VEIP)
+ continue;
+ ret = omci_agent_resolve_bridge_locked(
+ odev, &desired, object, &default_installed);
+ if (ret)
+ goto out;
+ }
+
+ ret = omci_agent_apply_services_locked(odev, &desired);
+
+out:
+ omci_agent_free_service_array(&desired);
+ xa_destroy(&desired);
+ return ret;
+}
+
+static struct omci_mib_object *
+omci_get_or_create_locked(struct omci_agent *agent, u16 class_id,
+  u16 entity_id, bool create)
+{
+ struct omci_mib_object *object;
+ struct omci_mib_object *stored;
+ int ret;
+
+ stored = omci_mib_lookup(agent, class_id, entity_id);
+ if (stored || !create)
+ return stored;
+
+ object = kzalloc(sizeof(*object), GFP_KERNEL);
+ if (!object)
+ return NULL;
+ object->class_id = class_id;
+ object->entity_id = entity_id;
+ object->origin = OMCI_MIB_ORIGIN_OLT;
+
+ ret = omci_mib_store_locked(agent, object);
+ kfree(object);
+ if (ret)
+ return NULL;
+
+ return omci_mib_lookup(agent, class_id, entity_id);
+}
+
+static u8 omci_agent_create_locked(struct omci_device *odev,
+   u16 class_id, u16 entity_id,
+   const u8 *content, size_t content_len)
+{
+ struct omci_agent *agent = &odev->agent;
+ const struct omci_me_desc *desc;
+ struct omci_mib_object *stored = NULL;
+ struct omci_mib_object *object;
+ u8 result = OMCI_RESULT_SUCCESS;
+ bool hardware_applied = false;
+ int ret;
+
+ if (!omci_me_active(agent, class_id))
+ return OMCI_RESULT_UNKNOWN_ME;
+ if (!omci_me_action_allowed(agent, class_id, OMCI_MSG_TYPE_CREATE))
+ return OMCI_RESULT_NOT_SUPPORTED;
+ if (omci_mib_lookup(agent, class_id, entity_id))
+ return OMCI_RESULT_INSTANCE_EXISTS;
+
+ desc = omci_me_lookup(agent, class_id);
+ object = kzalloc(sizeof(*object), GFP_KERNEL);
+ if (!object)
+ return OMCI_RESULT_DEVICE_BUSY;
+ object->class_id = class_id;
+ object->entity_id = entity_id;
+ object->origin = OMCI_MIB_ORIGIN_OLT;
+ object->owner_profile = desc && (desc->flags & OMCI_ME_F_VENDOR) ?
+ agent->profile_effective : OMCI_OLT_PROFILE_UNSPEC;
+ object->data_len = desc ? desc->data_len :
+ min_t(size_t, content_len, sizeof(object->data));
+
+ if (desc) {
+ ret = omci_me_decode_create(desc, object, content, content_len);
+ } else {
+ memcpy(object->data, content,
+       min_t(size_t, content_len, sizeof(object->data)));
+ object->attr_mask = 0xffff;
+ ret = 0;
+ }
+ if (ret) {
+ result = OMCI_RESULT_PARAMETER_ERROR;
+ goto out;
+ }
+ ret = omci_mib_parse_create(object, object->data);
+ if (ret) {
+ result = OMCI_RESULT_PARAMETER_ERROR;
+ goto out;
+ }
+ ret = omci_agent_hw_update(odev, object, OMCI_MSG_TYPE_CREATE,
+   content);
+ if (ret) {
+ result = OMCI_RESULT_PROCESSING_ERROR;
+ goto out;
+ }
+ hardware_applied = true;
+ ret = omci_mib_store_locked(agent, object);
+ if (ret) {
+ result = OMCI_RESULT_DEVICE_BUSY;
+ goto rollback;
+ }
+ stored = omci_mib_lookup(agent, class_id, entity_id);
+ if (omci_agent_datapath_class(agent, class_id)) {
+ ret = omci_agent_reconcile_services_locked(odev);
+ if (ret) {
+ result = OMCI_RESULT_PROCESSING_ERROR;
+ goto rollback;
+ }
+ }
+ omci_agent_increment_mib_sync_locked(agent);
+ goto out;
+
+rollback:
+ if (stored) {
+ xa_erase(&agent->mib, omci_mib_key(class_id, entity_id));
+ kfree(stored);
+ }
+ if (hardware_applied)
+ omci_agent_hw_update(odev, object, OMCI_MSG_TYPE_DELETE,
+     object->data);
+out:
+ kfree(object);
+ return result;
+}
+
+static u8 omci_agent_set_locked(struct omci_device *odev, u16 class_id,
+ u16 entity_id, u8 action, const u8 *content,
+ size_t content_len, bool *profile_changed)
+{
+ struct omci_agent *agent = &odev->agent;
+ const struct omci_me_desc *desc;
+ struct omci_mib_object *previous = NULL;
+ struct omci_mib_object *object;
+ struct omci_mib_object *existing;
+ bool hardware_applied = false;
+ bool allow_create;
+ bool existed;
+ u16 mask;
+ int ret;
+
+ if (content_len < 2)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ if (!omci_me_active(agent, class_id))
+ return OMCI_RESULT_UNKNOWN_ME;
+ if (!omci_me_action_allowed(agent, class_id, action))
+ return OMCI_RESULT_NOT_SUPPORTED;
+
+ mask = get_unaligned_be16(content);
+ desc = omci_me_lookup(agent, class_id);
+ allow_create = agent->permissive || agent->fake_omci ||
+ omci_agent_profile_has_quirk(agent,
+     OMCI_OLT_QUIRK_ALLOW_SET_CREATE);
+ existing = omci_mib_lookup(agent, class_id, entity_id);
+ existed = !!existing;
+ if (existing) {
+ previous = kmemdup(existing, sizeof(*previous), GFP_KERNEL);
+ if (!previous)
+ return OMCI_RESULT_DEVICE_BUSY;
+ }
+ object = omci_get_or_create_locked(agent, class_id, entity_id,
+  allow_create);
+ if (!object) {
+ ret = allow_create ? OMCI_RESULT_DEVICE_BUSY :
+     OMCI_RESULT_UNKNOWN_INSTANCE;
+ goto out_free_previous;
+ }
+ if (!object->data_len)
+ object->data_len = desc ? desc->data_len : sizeof(object->data);
+ if (!object->owner_profile && desc && (desc->flags & OMCI_ME_F_VENDOR))
+ object->owner_profile = agent->profile_effective;
+
+ if (desc) {
+ ret = omci_me_decode_set(desc, object, mask, content + 2,
+ content_len - 2);
+ } else if (class_id == OMCI_CLASS_PRIORITY_QUEUE ||
+   class_id == OMCI_CLASS_TRAFFIC_SCHEDULER) {
+ ret = omci_mib_parse_set(object, mask, content + 2,
+ content_len - 2);
+ } else {
+ memcpy(object->data, content + 2,
+       min_t(size_t, content_len - 2, sizeof(object->data)));
+ object->attr_mask |= mask;
+ ret = 0;
+ }
+ if (!ret && desc && (class_id == OMCI_CLASS_OLT_G ||
+     class_id == OMCI_CLASS_VLAN_TAGGING_FILTER_DATA ||
+     class_id == OMCI_CLASS_EXTENDED_VLAN))
+ ret = omci_mib_parse_set(object, mask, content + 2,
+ content_len - 2);
+ if (!ret && !desc && class_id == OMCI_CLASS_OLT_G)
+ ret = omci_mib_parse_set(object, mask, content + 2,
+ content_len - 2);
+ if (ret)
+ goto rollback_parameter;
+
+ /* Keep ONU-created instances across a subsequent MIB reset. */
+ if (!existed)
+ object->origin = OMCI_MIB_ORIGIN_OLT;
+ if (class_id == OMCI_CLASS_OLT_G) {
+ ret = omci_agent_profile_refresh_locked(odev, &object->olt_g,
+ profile_changed);
+ if (ret)
+ goto rollback_processing;
+ }
+ ret = omci_agent_hw_update(odev, object, action, content);
+ if (ret)
+ goto rollback_processing;
+ hardware_applied = true;
+ if (omci_agent_datapath_class(agent, class_id)) {
+ ret = omci_agent_reconcile_services_locked(odev);
+ if (ret)
+ goto rollback_processing;
+ }
+ omci_agent_increment_mib_sync_locked(agent);
+ ret = OMCI_RESULT_SUCCESS;
+ goto out_free_previous;
+
+rollback_parameter:
+ if (existed) {
+ *object = *previous;
+ } else {
+ xa_erase(&agent->mib, omci_mib_key(class_id, entity_id));
+ kfree(object);
+ }
+ ret = ret == -ENOSPC ? OMCI_RESULT_DEVICE_BUSY :
+     OMCI_RESULT_PARAMETER_ERROR;
+ goto out_free_previous;
+
+rollback_processing:
+ if (existed) {
+ *object = *previous;
+ if (hardware_applied)
+ omci_agent_hw_update(odev, object, OMCI_MSG_TYPE_SET,
+     object->data);
+ } else {
+ xa_erase(&agent->mib, omci_mib_key(class_id, entity_id));
+ if (hardware_applied)
+ omci_agent_hw_update(odev, object, OMCI_MSG_TYPE_DELETE,
+     object->data);
+ kfree(object);
+ }
+ ret = OMCI_RESULT_PROCESSING_ERROR;
+
+out_free_previous:
+ kfree(previous);
+ return ret;
+}
+
+static u8 omci_agent_delete_locked(struct omci_device *odev, u16 class_id,
+   u16 entity_id, bool *profile_changed)
+{
+ struct omci_agent *agent = &odev->agent;
+ const struct omci_me_desc *desc;
+ struct omci_mib_object *object;
+ u8 restore_action;
+ int ret;
+
+ if (!omci_me_active(agent, class_id))
+ return OMCI_RESULT_UNKNOWN_ME;
+ if (!omci_me_action_allowed(agent, class_id, OMCI_MSG_TYPE_DELETE))
+ return OMCI_RESULT_NOT_SUPPORTED;
+ object = omci_mib_lookup(agent, class_id, entity_id);
+ if (!object)
+ return OMCI_RESULT_UNKNOWN_INSTANCE;
+ if (object->origin == OMCI_MIB_ORIGIN_DEFAULT)
+ return OMCI_RESULT_NOT_SUPPORTED;
+
+ desc = omci_me_lookup(agent, class_id);
+ restore_action = desc && (desc->flags & OMCI_ME_F_ONU_CREATED) ?
+ OMCI_MSG_TYPE_SET : OMCI_MSG_TYPE_CREATE;
+ object->pending_delete = true;
+
+ if (class_id == OMCI_CLASS_OLT_G) {
+ ret = omci_agent_profile_refresh_locked(odev, NULL,
+ profile_changed);
+ if (ret)
+ goto rollback_mib;
+ }
+ if (omci_agent_datapath_class(agent, class_id)) {
+ ret = omci_agent_reconcile_services_locked(odev);
+ if (ret)
+ goto rollback_profile;
+ }
+ ret = omci_agent_hw_update(odev, object, OMCI_MSG_TYPE_DELETE,
+   object->data);
+ if (ret)
+ goto rollback_services;
+
+ xa_erase(&agent->mib, omci_mib_key(class_id, entity_id));
+ kfree(object);
+ omci_agent_increment_mib_sync_locked(agent);
+ return OMCI_RESULT_SUCCESS;
+
+rollback_services:
+ object->pending_delete = false;
+ omci_agent_hw_update(odev, object, restore_action, object->data);
+ omci_agent_reconcile_services_locked(odev);
+ return OMCI_RESULT_PROCESSING_ERROR;
+rollback_profile:
+ if (class_id == OMCI_CLASS_OLT_G)
+ omci_agent_profile_refresh_locked(odev, &object->olt_g, NULL);
+rollback_mib:
+ object->pending_delete = false;
+ return OMCI_RESULT_PROCESSING_ERROR;
+}
+
+static s16 omci_power_nw_to_0p002_db(u32 power_nw, s32 offset)
+{
+ s64 value;
+
+ if (!power_nw)
+ return -32768;
+
+ value = DIV_ROUND_CLOSEST_ULL((u64)intlog10(power_nw) *
+      OMCI_OPTICAL_LEVEL_SCALE * 10,
+      1U << 24);
+ value -= offset;
+
+ return clamp_t(s64, value, -32768, 32767);
+}
+
+static s16 omci_power_nw_to_ani_g_level(u32 power_nw)
+{
+ return omci_power_nw_to_0p002_db(power_nw,
+ OMCI_NW_TO_DBM_0P002_OFFSET);
+}
+
+static s16 omci_power_nw_to_test_level(u32 power_nw)
+{
+ return omci_power_nw_to_0p002_db(power_nw,
+ OMCI_NW_TO_DBUW_0P002_OFFSET);
+}
+
+static s16 omci_temperature_mc_to_test_level(s32 temperature_mc)
+{
+ s64 value;
+
+ value = DIV_S64_ROUND_CLOSEST((s64)temperature_mc * 256, 1000);
+
+ return clamp_t(s64, value, -32768, 32767);
+}
+
+static u16 omci_voltage_uv_to_test_level(u32 voltage_uv)
+{
+ return min_t(u32, DIV_ROUND_CLOSEST(voltage_uv, 20000), 65535);
+}
+
+static u16 omci_bias_ua_to_test_level(u32 bias_ua)
+{
+ return min_t(u32, DIV_ROUND_CLOSEST(bias_ua, 2), 65535);
+}
+
+static u8 omci_agent_get_ani_g_telemetry(struct omci_device *odev, u16 mask,
+ u8 *response_content,
+ size_t response_capacity,
+ size_t *response_len)
+{
+ struct omci_telemetry telemetry = {};
+ u8 *data = response_content + 3;
+ size_t used = 3;
+ u16 level;
+ int ret;
+
+ if (response_capacity < 3)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ memset(response_content, 0, response_capacity);
+ ret = odev->ops->get_telemetry(odev, &telemetry);
+ if (ret)
+ return OMCI_RESULT_PROCESSING_ERROR;
+
+ if ((mask & OMCI_ANI_G_RX_LEVEL_MASK) &&
+    !(telemetry.valid & OMCI_TELEMETRY_F_BOSA_RX_POWER))
+ goto attribute_failed;
+ if ((mask & OMCI_ANI_G_TX_LEVEL_MASK) &&
+    !(telemetry.valid & OMCI_TELEMETRY_F_BOSA_TX_POWER))
+ goto attribute_failed;
+ if (hweight16(mask & OMCI_ANI_G_OPTICAL_LEVEL_MASK) * sizeof(u16) >
+    response_capacity - used)
+ goto attribute_failed;
+
+ response_content[0] = OMCI_RESULT_SUCCESS;
+ put_unaligned_be16(mask, response_content + 1);
+ if (mask & OMCI_ANI_G_RX_LEVEL_MASK) {
+ level = (u16)omci_power_nw_to_ani_g_level(
+ telemetry.bosa_rx_power_nw);
+ put_unaligned_be16(level, data);
+ data += 2;
+ used += 2;
+ }
+ if (mask & OMCI_ANI_G_TX_LEVEL_MASK) {
+ level = (u16)omci_power_nw_to_ani_g_level(
+ telemetry.bosa_tx_power_nw);
+ put_unaligned_be16(level, data);
+ used += 2;
+ }
+ *response_len = used;
+ return OMCI_RESULT_SUCCESS;
+
+attribute_failed:
+ if (response_capacity < 5)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ response_content[0] = OMCI_RESULT_ATTRIBUTE_FAILED;
+ put_unaligned_be16(0, response_content + 1);
+ put_unaligned_be16(mask, response_content + 3);
+ *response_len = 5;
+ return OMCI_RESULT_ATTRIBUTE_FAILED;
+}
+
+static bool omci_agent_get_uses_ani_g_telemetry(u16 class_id, u16 entity_id,
+ u16 mask)
+{
+ return class_id == OMCI_CLASS_ANI_G &&
+       entity_id == OMCI_ANI_G_ENTITY_ID &&
+       mask &&
+       !(mask & ~OMCI_ANI_G_OPTICAL_LEVEL_MASK);
+}
+
+static u8 omci_agent_get_masked_object(const struct omci_mib_object *object,
+       const struct omci_get_attr_layout *layout,
+       size_t layout_len, u16 supported_mask,
+       u16 mask, u8 *response_content,
+       size_t response_capacity,
+       size_t *response_len)
+{
+ u8 *data = response_content + 3;
+ size_t available;
+ size_t used = 3;
+ unsigned int i;
+
+ if (response_capacity < 3)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ memset(response_content, 0, response_capacity);
+ available = response_capacity - used;
+ if (!mask || mask & ~supported_mask)
+ goto attribute_failed;
+
+ for (i = 0; i < layout_len; i++) {
+ const struct omci_get_attr_layout *attr = &layout[i];
+
+ if (!(mask & attr->mask))
+ continue;
+ if (attr->len > available)
+ goto attribute_failed;
+
+ if (attr->offset == OMCI_ATTR_VALUE_ZERO)
+ memset(data, 0, attr->len);
+ else
+ memcpy(data, object->data + attr->offset, attr->len);
+ data += attr->len;
+ used += attr->len;
+ available -= attr->len;
+ }
+
+ response_content[0] = OMCI_RESULT_SUCCESS;
+ put_unaligned_be16(mask, response_content + 1);
+ *response_len = used;
+ return OMCI_RESULT_SUCCESS;
+
+attribute_failed:
+ if (response_capacity < 5)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ response_content[0] = OMCI_RESULT_ATTRIBUTE_FAILED;
+ put_unaligned_be16(0, response_content + 1);
+ put_unaligned_be16(mask, response_content + 3);
+ *response_len = 5;
+ return OMCI_RESULT_ATTRIBUTE_FAILED;
+}
+
+static int
+omci_agent_refresh_nokia_optical(struct omci_device *odev,
+ struct omci_mib_object *object)
+{
+ struct omci_telemetry telemetry = {};
+ u16 value;
+ int ret;
+
+ if (!odev->ops->get_telemetry)
+ return -EOPNOTSUPP;
+ ret = odev->ops->get_telemetry(odev, &telemetry);
+ if (ret)
+ return ret;
+
+ object->data[0] = !!(telemetry.valid & OMCI_TELEMETRY_F_BOSA_VOLTAGE);
+ if (object->data[0]) {
+ value = min_t(u32, DIV_ROUND_CLOSEST(telemetry.bosa_voltage_uv,
+  10000), U16_MAX);
+ put_unaligned_be16(value, object->data + 1);
+ }
+ object->data[3] = !!(telemetry.valid & OMCI_TELEMETRY_F_BOSA_RX_POWER);
+ if (object->data[3]) {
+ value = min_t(u32, DIV_ROUND_CLOSEST(telemetry.bosa_rx_power_nw,
+  10000), U16_MAX);
+ put_unaligned_be16(value, object->data + 4);
+ }
+ object->data[6] = !!(telemetry.valid & OMCI_TELEMETRY_F_BOSA_TX_POWER);
+ if (object->data[6]) {
+ value = min_t(u32, DIV_ROUND_CLOSEST(telemetry.bosa_tx_power_nw,
+  10000), U16_MAX);
+ put_unaligned_be16(value, object->data + 7);
+ }
+ object->data[9] = !!(telemetry.valid & OMCI_TELEMETRY_F_BOSA_BIAS);
+ if (object->data[9]) {
+ value = min_t(u64, DIV_ROUND_CLOSEST_ULL(
+ (u64)telemetry.bosa_bias_ua * 2, 1000000),
+      U16_MAX);
+ put_unaligned_be16(value, object->data + 10);
+ }
+ object->data[12] = !!(telemetry.valid &
+      OMCI_TELEMETRY_F_BOSA_TEMPERATURE);
+ if (object->data[12]) {
+ value = clamp_t(s32, DIV_S64_ROUND_CLOSEST(
+ telemetry.bosa_temperature_mc, 1000),
+ 0, U16_MAX);
+ put_unaligned_be16(value, object->data + 13);
+ }
+
+ return 0;
+}
+
+static u8 omci_agent_get_locked(struct omci_device *odev, u16 class_id,
+ u16 entity_id, const u8 *content,
+ size_t content_len, u8 *response_content,
+ size_t response_capacity,
+ size_t *response_len)
+{
+ struct omci_agent *agent = &odev->agent;
+ const struct omci_me_desc *desc;
+ struct omci_mib_object *object;
+ size_t encoded_len = 0;
+ u16 encoded_mask;
+ u16 mask;
+ int ret;
+
+ if (content_len < 2 || response_capacity < 1)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ mask = get_unaligned_be16(content);
+ if (!omci_me_active(agent, class_id))
+ return OMCI_RESULT_UNKNOWN_ME;
+ if (!omci_me_action_allowed(agent, class_id, OMCI_MSG_TYPE_GET))
+ return OMCI_RESULT_NOT_SUPPORTED;
+ if (class_id == OMCI_CLASS_OMCI)
+ return omci_agent_get_omci_table_locked(odev, entity_id, mask,
+ response_content,
+ response_capacity,
+ response_len);
+ if (class_id == OMCI_CLASS_MANAGED_ENTITY)
+ return omci_agent_get_managed_entity_locked(odev, entity_id,
+ mask, response_content, response_capacity, response_len);
+ if (class_id == OMCI_CLASS_ATTRIBUTE)
+ return omci_agent_get_attribute_locked(odev, entity_id, mask,
+ response_content, response_capacity, response_len);
+
+ if (omci_agent_get_uses_ani_g_telemetry(class_id, entity_id, mask) &&
+    odev->ops && odev->ops->get_telemetry)
+ return omci_agent_get_ani_g_telemetry(odev, mask,
+      response_content,
+      response_capacity,
+      response_len);
+
+ object = omci_get_or_create_locked(agent, class_id, entity_id,
+   agent->permissive ||
+   agent->fake_omci);
+ if (!object)
+ return OMCI_RESULT_UNKNOWN_INSTANCE;
+ if (class_id == OMCI_CLASS_NOKIA_OPTICAL_SUPERVISION &&
+    omci_agent_refresh_nokia_optical(odev, object))
+ return OMCI_RESULT_PROCESSING_ERROR;
+
+ switch (class_id) {
+ case OMCI_CLASS_ONU_G:
+ return omci_agent_get_masked_object(
+ object, omci_onu_g_attr_layout,
+ ARRAY_SIZE(omci_onu_g_attr_layout), OMCI_ONU_G_ATTR_MASK,
+ mask, response_content, response_capacity, response_len);
+ case OMCI_CLASS_ONU2_G:
+ return omci_agent_get_masked_object(
+ object, omci_onu2_g_attr_layout,
+ ARRAY_SIZE(omci_onu2_g_attr_layout), OMCI_ONU2_G_ATTR_MASK,
+ mask, response_content, response_capacity, response_len);
+ default:
+ break;
+ }
+
+ if (response_capacity < 3)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ memset(response_content, 0, response_capacity);
+ response_content[0] = OMCI_RESULT_SUCCESS;
+ if (class_id == OMCI_CLASS_PRIORITY_QUEUE) {
+ put_unaligned_be16(mask, response_content + 1);
+ ret = omci_priority_queue_serialize(object, mask,
+    response_content + 3,
+    response_capacity - 3,
+    &encoded_len);
+ if (ret)
+ goto attribute_failed;
+ } else if (class_id == OMCI_CLASS_TRAFFIC_SCHEDULER) {
+ put_unaligned_be16(mask, response_content + 1);
+ ret = omci_traffic_scheduler_serialize(object, mask,
+       response_content + 3,
+       response_capacity - 3,
+       &encoded_len);
+ if (ret)
+ goto attribute_failed;
+ } else {
+ desc = omci_me_lookup(agent, class_id);
+ if (!desc) {
+ encoded_len = min_t(size_t, object->data_len,
+    response_capacity - 3);
+ put_unaligned_be16(mask, response_content + 1);
+ memcpy(response_content + 3, object->data, encoded_len);
+ *response_len = 3 + encoded_len;
+ return OMCI_RESULT_SUCCESS;
+ }
+ ret = omci_me_encode_attributes(desc, object, mask,
+ response_content + 3,
+ response_capacity - 3,
+ &encoded_mask, &encoded_len);
+ if (ret || encoded_mask != mask)
+ goto attribute_failed;
+ put_unaligned_be16(encoded_mask, response_content + 1);
+ }
+ *response_len = 3 + encoded_len;
+ return OMCI_RESULT_SUCCESS;
+
+attribute_failed:
+ if (response_capacity < 5)
+ return OMCI_RESULT_PARAMETER_ERROR;
+ response_content[0] = OMCI_RESULT_ATTRIBUTE_FAILED;
+ put_unaligned_be16(0, response_content + 1);
+ put_unaligned_be16(mask, response_content + 3);
+ *response_len = 5;
+ return OMCI_RESULT_ATTRIBUTE_FAILED;
+}
+
+static void omci_test_result_put(u8 *field, u8 type, u16 value)
+{
+ field[0] = type;
+ put_unaligned_be16(value, field + 1);
+}
+
+static bool omci_agent_is_ani_g_test(const struct omci_wire_request *request)
+{
+ return (request->message_type & 0x1f) == OMCI_MSG_TYPE_TEST &&
+       request->class_id == OMCI_CLASS_ANI_G &&
+       request->entity_id == OMCI_ANI_G_ENTITY_ID;
+}
+
+static void omci_agent_log_wire(struct omci_device *odev,
+ const char *direction, const void *data,
+ size_t len)
+{
+ struct omci_wire_request request;
+ bool response;
+ u8 action;
+
+ if (omci_wire_decode(data, len, &request)) {
+ dev_warn(odev->parent, "OMCI %s invalid PDU: %*phN\n",
+ direction, (int)len, data);
+ return;
+ }
+
+ action = request.message_type & 0x1f;
+ response = request.message_type & 0x20;
+ if (action == OMCI_MSG_TYPE_MIB_UPLOAD && response &&
+    request.payload_len >= 2) {
+ dev_dbg(odev->parent,
+ "OMCI %s: tci=%#06x type=%#04x action=%u device=%#04x class=%u
entity=%#06x payload=%u command-count=%u\n",
+ direction, request.transaction_id, request.message_type,
+ action, request.device_id, request.class_id,
+ request.entity_id, request.payload_len,
+ get_unaligned_be16(request.payload));
+ } else if (action == OMCI_MSG_TYPE_MIB_UPLOAD_NEXT &&
+   request.payload_len >= 2) {
+ dev_dbg(odev->parent,
+ "OMCI %s: tci=%#06x type=%#04x action=%u device=%#04x class=%u
entity=%#06x payload=%u %s=%u\n",
+ direction, request.transaction_id, request.message_type,
+ action, request.device_id, request.class_id,
+ request.entity_id, request.payload_len,
+ response ? "returned-class" : "sequence",
+ get_unaligned_be16(request.payload));
+ } else if (action == OMCI_MSG_TYPE_GET_NEXT &&
+   request.payload_len >= (response ? 3 : 4)) {
+ u16 mask = response ? get_unaligned_be16(request.payload + 1) :
+      get_unaligned_be16(request.payload);
+ u16 sequence = response ? 0 :
+  get_unaligned_be16(request.payload + 2);
+
+ dev_dbg(odev->parent,
+ "OMCI %s: tci=%#06x type=%#04x action=%u device=%#04x class=%u
entity=%#06x payload=%u result=%u mask=%#06x sequence=%u\n",
+ direction, request.transaction_id, request.message_type,
+ action, request.device_id, request.class_id,
+ request.entity_id, request.payload_len,
+ response ? request.payload[0] : 0, mask, sequence);
+ } else {
+ u8 result = request.payload_len ? request.payload[0] : 0;
+
+ dev_dbg(odev->parent,
+ "OMCI %s: tci=%#06x type=%#04x action=%u device=%#04x class=%u
entity=%#06x payload=%u result=%u\n",
+ direction, request.transaction_id, request.message_type,
+ action, request.device_id, request.class_id,
+ request.entity_id, request.payload_len, result);
+ }
+ dev_dbg(odev->parent, "OMCI %s PDU: %*phN\n",
+ direction, (int)len, data);
+}
+
+static int
+omci_agent_send_ani_g_test_result(struct omci_device *odev,
+  const struct omci_wire_request *request)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_telemetry telemetry = {};
+ u8 pdu[OMCI_BASELINE_LEN_NO_MIC];
+ u8 content[17] = {};
+ size_t pdu_len;
+ u16 value;
+ int ret;
+
+ if (!odev->ops || !odev->ops->get_telemetry)
+ return -EOPNOTSUPP;
+
+ ret = odev->ops->get_telemetry(odev, &telemetry);
+ if (ret)
+ return ret;
+
+ if (telemetry.valid & OMCI_TELEMETRY_F_BOSA_VOLTAGE) {
+ value = omci_voltage_uv_to_test_level(telemetry.bosa_voltage_uv);
+ omci_test_result_put(content, OMCI_TEST_TYPE_VOLTAGE, value);
+ } else {
+ omci_test_result_put(content, OMCI_TEST_TYPE_UNSUPPORTED, 0);
+ }
+
+ if (telemetry.valid & OMCI_TELEMETRY_F_BOSA_RX_POWER) {
+ value = (u16)omci_power_nw_to_test_level(
+ telemetry.bosa_rx_power_nw);
+ omci_test_result_put(content + 3, OMCI_TEST_TYPE_RX_POWER,
+     value);
+ } else {
+ omci_test_result_put(content + 3, OMCI_TEST_TYPE_UNSUPPORTED, 0);
+ }
+
+ if (telemetry.valid & OMCI_TELEMETRY_F_BOSA_TX_POWER) {
+ value = (u16)omci_power_nw_to_test_level(
+ telemetry.bosa_tx_power_nw);
+ omci_test_result_put(content + 6, OMCI_TEST_TYPE_TX_POWER,
+     value);
+ } else {
+ omci_test_result_put(content + 6, OMCI_TEST_TYPE_UNSUPPORTED, 0);
+ }
+
+ if (telemetry.valid & OMCI_TELEMETRY_F_BOSA_BIAS) {
+ value = omci_bias_ua_to_test_level(telemetry.bosa_bias_ua);
+ omci_test_result_put(content + 9, OMCI_TEST_TYPE_BIAS, value);
+ } else {
+ omci_test_result_put(content + 9, OMCI_TEST_TYPE_UNSUPPORTED, 0);
+ }
+
+ if (telemetry.valid & OMCI_TELEMETRY_F_BOSA_TEMPERATURE) {
+ value = (u16)omci_temperature_mc_to_test_level(
+ telemetry.bosa_temperature_mc);
+ omci_test_result_put(content + 12, OMCI_TEST_TYPE_TEMPERATURE,
+     value);
+ } else {
+ omci_test_result_put(content + 12, OMCI_TEST_TYPE_UNSUPPORTED, 0);
+ }
+ put_unaligned_be16(0, content + 15);
+
+ ret = omci_wire_encode_response(request, OMCI_MSG_TYPE_TEST_RESULT,
+ content, sizeof(content), pdu,
+ sizeof(pdu), &pdu_len);
+ if (ret)
+ return ret;
+
+ omci_agent_log_wire(odev, "TX", pdu, pdu_len);
+ ret = omci_device_xmit(odev, pdu, pdu_len);
+ if (!ret)
+ atomic64_inc(&agent->responses);
+
+ return ret;
+}
+
+static int omci_agent_upload_next_locked(struct omci_agent *agent,
+ u16 sequence, u8 *content)
+{
+ const struct omci_me_desc *desc;
+ struct omci_mib_object *object;
+ unsigned long index;
+ size_t encoded_len = 0;
+ u16 encoded_mask = 0;
+ u16 upload_pos = 0;
+ int ret;
+
+ xa_for_each(&agent->mib, index, object) {
+ if (!omci_mib_object_uploadable(agent, object))
+ continue;
+ if (upload_pos++ == sequence)
+ goto found;
+ }
+
+ return -ENOENT;
+
+found:
+ put_unaligned_be16(object->class_id, content);
+ put_unaligned_be16(object->entity_id, content + 2);
+ desc = omci_me_lookup(agent, object->class_id);
+ if (desc) {
+ u16 mask = object->attr_mask & desc->mib_upload_mask;
+
+ ret = omci_me_encode_attributes(desc, object, mask,
+ content + 6, 26,
+ &encoded_mask, &encoded_len);
+ if (ret && mask)
+ return ret;
+ } else {
+ encoded_mask = object->attr_mask;
+ encoded_len = min_t(size_t, object->data_len ?: 26, 26);
+ memcpy(content + 6, object->data, encoded_len);
+ }
+ put_unaligned_be16(encoded_mask, content + 4);
+ if (encoded_len < 26)
+ memset(content + 6 + encoded_len, 0, 26 - encoded_len);
+ agent->upload_index = sequence + 1;
+ return 0;
+}
+
+static int
+omci_agent_build_response_locked(struct omci_device *odev,
+ const struct omci_wire_request *request,
+ u8 *content, size_t capacity,
+ size_t *content_len, bool *unsupported,
+ bool *fake, bool *profile_changed)
+{
+ struct omci_agent *agent = &odev->agent;
+ u8 action = request->message_type & 0x1f;
+ u16 class_id = request->class_id;
+ u16 entity_id = request->entity_id;
+ bool shadow_missing;
+ u16 sequence;
+ u16 mask = 0;
+ u8 result = OMCI_RESULT_SUCCESS;
+ int ret;
+
+ if (!capacity)
+ return -EMSGSIZE;
+ memset(content, 0, capacity);
+ shadow_missing = !omci_mib_lookup(agent, class_id, entity_id);
+ *unsupported = false;
+ *fake = false;
+ *profile_changed = false;
+ *content_len = 1;
+
+ switch (action) {
+ case OMCI_MSG_TYPE_CREATE:
+ result = omci_agent_create_locked(odev, class_id, entity_id,
+ request->payload,
+ request->payload_len);
+ if (omci_agent_should_fake_result(agent, class_id, result)) {
+ result = OMCI_RESULT_SUCCESS;
+ *fake = true;
+ }
+ content[0] = result;
+ break;
+ case OMCI_MSG_TYPE_DELETE:
+ result = omci_agent_delete_locked(odev, class_id, entity_id,
+ profile_changed);
+ if (omci_agent_should_fake_result(agent, class_id, result)) {
+ result = OMCI_RESULT_SUCCESS;
+ *fake = true;
+ }
+ content[0] = result;
+ break;
+ case OMCI_MSG_TYPE_SET:
+ case OMCI_MSG_TYPE_SET_TABLE:
+ result = omci_agent_set_locked(odev, class_id, entity_id, action,
+      request->payload,
+      request->payload_len,
+      profile_changed);
+ if (omci_agent_should_fake_result(agent, class_id, result)) {
+ result = OMCI_RESULT_SUCCESS;
+ *fake = true;
+ } else if (!result && omci_agent_fakes_unsupported(agent) &&
+   !agent->permissive && shadow_missing) {
+ *fake = true;
+ }
+ if (request->payload_len >= 2)
+ mask = get_unaligned_be16(request->payload);
+ if (capacity < 5)
+ return -EMSGSIZE;
+ content[0] = result;
+ put_unaligned_be16(0, content + 1);
+ put_unaligned_be16(result ? mask : 0, content + 3);
+ *content_len = 5;
+ break;
+ case OMCI_MSG_TYPE_GET:
+ case OMCI_MSG_TYPE_GET_CURRENT_DATA:
+ result = omci_agent_get_locked(odev, class_id, entity_id,
+      request->payload,
+      request->payload_len, content,
+      capacity, content_len);
+ if (omci_agent_should_fake_result(agent, class_id, result)) {
+ content[0] = OMCI_RESULT_SUCCESS;
+ *content_len = 1;
+ *fake = true;
+ } else if (!result && omci_agent_fakes_unsupported(agent) &&
+   !agent->permissive && shadow_missing) {
+ *fake = true;
+ }
+ if (result && !*fake) {
+ content[0] = result;
+ *content_len = 1;
+ }
+ break;
+ case OMCI_MSG_TYPE_GET_NEXT:
+ result = omci_agent_get_next_locked(odev, class_id, entity_id,
+    request->device_id,
+    request->payload,
+    request->payload_len,
+    content, capacity,
+    content_len);
+ if (result) {
+ content[0] = result;
+ *content_len = 1;
+ }
+ break;
+ case OMCI_MSG_TYPE_GET_ALL_ALARMS:
+ if (capacity < 3)
+ return -EMSGSIZE;
+ content[0] = OMCI_RESULT_SUCCESS;
+ put_unaligned_be16(0, content + 1);
+ *content_len = 3;
+ break;
+ case OMCI_MSG_TYPE_GET_ALL_ALARMS_NEXT:
+ content[0] = OMCI_RESULT_UNKNOWN_INSTANCE;
+ break;
+ case OMCI_MSG_TYPE_MIB_UPLOAD:
+ if (capacity < 2)
+ return -EMSGSIZE;
+ agent->upload_index = 0;
+ put_unaligned_be16(omci_mib_count_locked(agent), content);
+ *content_len = 2;
+ break;
+ case OMCI_MSG_TYPE_MIB_UPLOAD_NEXT:
+ if (request->payload_len < 2 || capacity < 32)
+ return -EMSGSIZE;
+ sequence = get_unaligned_be16(request->payload);
+ ret = omci_agent_upload_next_locked(agent, sequence, content);
+ if (ret)
+ memset(content, 0, 32);
+ *content_len = 32;
+ break;
+ case OMCI_MSG_TYPE_MIB_RESET:
+ if (omci_agent_mib_reset_locked(odev, false, profile_changed))
+ content[0] = OMCI_RESULT_PROCESSING_ERROR;
+ else
+ content[0] = OMCI_RESULT_SUCCESS;
+ break;
+ case OMCI_MSG_TYPE_SYNC_TIME:
+ case OMCI_MSG_TYPE_REBOOT:
+ case OMCI_MSG_TYPE_TEST:
+ content[0] = OMCI_RESULT_SUCCESS;
+ break;
+ case OMCI_MSG_TYPE_START_SW_DOWNLOAD:
+ case OMCI_MSG_TYPE_DOWNLOAD_SECTION:
+ case OMCI_MSG_TYPE_END_SW_DOWNLOAD:
+ case OMCI_MSG_TYPE_ACTIVATE_SW:
+ case OMCI_MSG_TYPE_COMMIT_SW:
+ default:
+ if (omci_agent_fakes_unsupported(agent)) {
+ content[0] = OMCI_RESULT_SUCCESS;
+ *fake = true;
+ } else {
+ content[0] = OMCI_RESULT_NOT_SUPPORTED;
+ *unsupported = true;
+ }
+ break;
+ }
+
+ return 0;
+}
+
+static void omci_agent_reset_duplicate_locked(struct omci_agent *agent)
+{
+ dev_kfree_skb(agent->last_request);
+ dev_kfree_skb(agent->last_response);
+ agent->last_request = NULL;
+ agent->last_response = NULL;
+ agent->last_request_hash = 0;
+ agent->last_response_fake = false;
+}
+
+static int
+omci_agent_cache_exchange_locked(struct omci_agent *agent,
+ const struct sk_buff *request,
+ const void *response, size_t response_len,
+ bool fake)
+{
+ struct sk_buff *cached_request;
+ struct sk_buff *cached_response;
+
+ cached_request = alloc_skb(request->len, GFP_KERNEL);
+ if (!cached_request)
+ return -ENOMEM;
+ skb_put_data(cached_request, request->data, request->len);
+ cached_response = alloc_skb(response_len, GFP_KERNEL);
+ if (!cached_response) {
+ kfree_skb(cached_request);
+ return -ENOMEM;
+ }
+ skb_put_data(cached_response, response, response_len);
+
+ omci_agent_reset_duplicate_locked(agent);
+ agent->last_request = cached_request;
+ agent->last_response = cached_response;
+ agent->last_request_hash = jhash(request->data, request->len, 0);
+ agent->last_response_fake = fake;
+ return 0;
+}
+
+static void
+omci_agent_report_operational(struct omci_device *odev, bool operational)
+{
+ if (odev->ops->set_operational)
+ odev->ops->set_operational(odev, operational);
+ omci_device_notify(odev, OMCI_EVENT_OPERATIONAL_CHANGE);
+}
+
+void omci_agent_receive(struct omci_device *odev, const struct sk_buff *skb)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_wire_request request;
+ u8 *response = NULL;
+ u8 *content = NULL;
+ size_t response_len = 0;
+ size_t content_len = 0;
+ size_t content_capacity;
+ u32 request_hash;
+ bool unsupported = false;
+ bool duplicate = false;
+ bool fake = false;
+ bool profile_changed = false;
+ bool operational_changed = false;
+ bool channel_up;
+ bool ani_g_test;
+ int ret;
+
+ ret = omci_wire_decode(skb->data, skb->len, &request);
+ if (ret)
+ return;
+ omci_agent_log_wire(odev, "RX", skb->data, skb->len);
+ ani_g_test = omci_agent_is_ani_g_test(&request);
+ request_hash = jhash(skb->data, skb->len, 0);
+
+ response = kmalloc(OMCI_MAX_PDU_LEN, GFP_KERNEL);
+ content_capacity = request.device_id == OMCI_BASELINE_DEV_ID ? 32 :
+ OMCI_MAX_PDU_LEN - OMCI_EXTENDED_HEADER_LEN;
+ content = kzalloc(content_capacity, GFP_KERNEL);
+ if (!response || !content)
+ goto out;
+
+ mutex_lock(&agent->lock);
+ if (!agent->enabled) {
+ mutex_unlock(&agent->lock);
+ goto out;
+ }
+
+ if (agent->last_request && agent->last_response &&
+    agent->last_request_hash == request_hash &&
+    agent->last_request->len == skb->len &&
+    !memcmp(agent->last_request->data, skb->data, skb->len)) {
+ response_len = agent->last_response->len;
+ memcpy(response, agent->last_response->data, response_len);
+ fake = agent->last_response_fake;
+ duplicate = true;
+ } else {
+ ret = omci_agent_build_response_locked(
+ odev, &request, content, content_capacity, &content_len,
+ &unsupported, &fake, &profile_changed);
+ if (!ret)
+ ret = omci_wire_encode_response(
+ &request, request.message_type & 0x1f,
+ content, content_len, response, OMCI_MAX_PDU_LEN,
+ &response_len);
+ if (ret) {
+ mutex_unlock(&agent->lock);
+ goto out;
+ }
+ omci_agent_cache_exchange_locked(agent, skb, response,
+ response_len, fake);
+ }
+ mutex_unlock(&agent->lock);
+
+ omci_agent_log_wire(odev, "TX", response, response_len);
+ ret = omci_device_xmit(odev, response, response_len);
+ if (!ret) {
+ atomic64_inc(&agent->responses);
+ if (!duplicate && !fake && !unsupported) {
+ spin_lock_bh(&odev->state_lock);
+ channel_up = odev->channel_up;
+ spin_unlock_bh(&odev->state_lock);
+
+ mutex_lock(&agent->lock);
+ if (channel_up && agent->enabled && !agent->operational) {
+ agent->operational = true;
+ operational_changed = true;
+ }
+ mutex_unlock(&agent->lock);
+ }
+ if (ani_g_test) {
+ ret = omci_agent_send_ani_g_test_result(odev, &request);
+ if (ret)
+ dev_dbg(odev->parent,
+ "OMCI ANI-G test result unavailable: %d\n",
+ ret);
+ }
+ }
+ if (duplicate)
+ atomic64_inc(&agent->duplicates);
+ if (unsupported) {
+ atomic64_inc(&agent->unsupported);
+ omci_device_notify(odev, OMCI_EVENT_UNSUPPORTED);
+ }
+ if (fake)
+ atomic64_inc(&agent->fake_responses);
+ if (profile_changed)
+ omci_device_notify(odev, OMCI_EVENT_PROFILE_CHANGE);
+ if (operational_changed)
+ omci_agent_report_operational(odev, true);
+
+out:
+ kfree(content);
+ kfree(response);
+}
+
+int omci_agent_send_dying_gasp(struct omci_device *odev)
+{
+ struct omci_agent *agent = &odev->agent;
+ u8 pdu[OMCI_BASELINE_LEN_NO_MIC] = {};
+ u8 sequence;
+ bool enabled;
+ int ret;
+
+ mutex_lock(&agent->lock);
+ enabled = agent->enabled && agent->dying_gasp;
+ if (enabled) {
+ sequence = ++agent->alarm_sequence;
+ if (!sequence)
+ sequence = ++agent->alarm_sequence;
+ }
+ mutex_unlock(&agent->lock);
+ if (!enabled)
+ return -EOPNOTSUPP;
+
+ pdu[2] = OMCI_MSG_TYPE_ALARM_NOTIFICATION;
+ pdu[3] = OMCI_BASELINE_DEV_ID;
+ put_unaligned_be16(OMCI_CLASS_ONU_G, pdu + 4);
+ put_unaligned_be16(0, pdu + 6);
+ pdu[8 + OMCI_ONU_G_DYING_GASP_ALARM / 8] |=
+ BIT(7 - (OMCI_ONU_G_DYING_GASP_ALARM % 8));
+ pdu[OMCI_ALARM_SEQUENCE_OFFSET] = sequence;
+ put_unaligned_be32(40, pdu + 40);
+
+ ret = omci_device_xmit(odev, pdu, sizeof(pdu));
+ if (ret)
+ return ret;
+
+ omci_agent_log_wire(odev, "TX", pdu, sizeof(pdu));
+ omci_device_notify(odev, OMCI_EVENT_DYING_GASP);
+ return 0;
+}
+
+void omci_agent_channel_changed(struct omci_device *odev, bool valid)
+{
+ struct omci_agent *agent = &odev->agent;
+ bool operational_changed;
+
+ mutex_lock(&agent->lock);
+ operational_changed = agent->operational;
+ agent->operational = false;
+ omci_agent_reset_duplicate_locked(agent);
+ omci_agent_reset_table_snapshot_locked(agent);
+ agent->upload_index = 0;
+ if (!valid) {
+ agent->alarm_sequence = 0;
+ omci_agent_clear_services_locked(odev);
+ } else {
+ omci_agent_reconcile_services_locked(odev);
+ }
+ mutex_unlock(&agent->lock);
+
+ if (operational_changed)
+ omci_agent_report_operational(odev, false);
+}
+
+int omci_agent_put_status(struct sk_buff *msg, struct omci_device *odev)
+{
+ struct omci_agent *agent = &odev->agent;
+ u32 count;
+ u32 profile_quirks;
+ u16 sync;
+ u8 profile_configured;
+ u8 profile_effective;
+ u8 profile_forced;
+ u8 onu_type;
+ bool enabled;
+ bool permissive;
+ bool fake_omci;
+ bool dying_gasp;
+ bool operational;
+
+ mutex_lock(&agent->lock);
+ count = omci_mib_count_locked(agent);
+ sync = agent->mib_sync;
+ enabled = agent->enabled;
+ permissive = agent->permissive;
+ fake_omci = agent->fake_omci;
+ dying_gasp = agent->dying_gasp;
+ operational = agent->operational;
+ profile_configured = agent->config.olt_profile;
+ profile_effective = agent->profile_effective;
+ profile_forced = agent->config.olt_profile_force;
+ onu_type = agent->config.onu_type;
+ profile_quirks = agent->profile_quirks;
+ mutex_unlock(&agent->lock);
+
+ if (nla_put_u8(msg, OMCI_ATTR_AGENT_ENABLED, enabled) ||
+    nla_put_u8(msg, OMCI_ATTR_AGENT_OPERATIONAL, operational) ||
+    nla_put_u8(msg, OMCI_ATTR_AGENT_PERMISSIVE, permissive) ||
+    nla_put_u8(msg, OMCI_ATTR_AGENT_FAKE_OMCI, fake_omci) ||
+    nla_put_u8(msg, OMCI_ATTR_AGENT_DYING_GASP, dying_gasp) ||
+    nla_put_u8(msg, OMCI_ATTR_ONU_TYPE, onu_type) ||
+    nla_put_u8(msg, OMCI_ATTR_OLT_PROFILE_CONFIGURED,
+       profile_configured) ||
+    nla_put_u8(msg, OMCI_ATTR_OLT_PROFILE_EFFECTIVE,
+       profile_effective) ||
+    nla_put_u8(msg, OMCI_ATTR_OLT_PROFILE_FORCED, profile_forced) ||
+    nla_put_u32(msg, OMCI_ATTR_OLT_PROFILE_QUIRKS, profile_quirks) ||
+    nla_put_u16(msg, OMCI_ATTR_MIB_SYNC, sync) ||
+    nla_put_u32(msg, OMCI_ATTR_MIB_OBJECTS, count) ||
+    nla_put_u64_64bit(msg, OMCI_ATTR_AGENT_RESPONSES,
+      atomic64_read(&agent->responses), OMCI_ATTR_PAD) ||
+    nla_put_u64_64bit(msg, OMCI_ATTR_AGENT_DUPLICATES,
+      atomic64_read(&agent->duplicates), OMCI_ATTR_PAD) ||
+    nla_put_u64_64bit(msg, OMCI_ATTR_AGENT_UNSUPPORTED,
+      atomic64_read(&agent->unsupported), OMCI_ATTR_PAD) ||
+    nla_put_u64_64bit(msg, OMCI_ATTR_AGENT_FAKE_RESPONSES,
+      atomic64_read(&agent->fake_responses), OMCI_ATTR_PAD))
+ return -EMSGSIZE;
+ return 0;
+}
+
+int omci_agent_config_get(struct omci_device *odev, u16 key,
+  void *value, size_t *len)
+{
+ struct omci_agent *agent = &odev->agent;
+ const void *source = NULL;
+ size_t source_len = 0;
+ u8 scalar;
+
+ mutex_lock(&agent->lock);
+ switch (key) {
+ case OMCI_CONFIG_SERIAL_NUMBER:
+ source = agent->config.serial_number;
+ source_len = sizeof(agent->config.serial_number);
+ break;
+ case OMCI_CONFIG_VENDOR_ID:
+ source = agent->config.vendor_id;
+ source_len = sizeof(agent->config.vendor_id);
+ break;
+ case OMCI_CONFIG_VERSION:
+ case OMCI_CONFIG_HARDWARE_VERSION:
+ source = agent->config.version;
+ source_len = strnlen(agent->config.version,
+     sizeof(agent->config.version));
+ break;
+ case OMCI_CONFIG_SOFTWARE_VERSION_0:
+ source = agent->config.software_version[0];
+ source_len = strnlen(agent->config.software_version[0],
+     sizeof(agent->config.software_version[0]));
+ break;
+ case OMCI_CONFIG_SOFTWARE_VERSION_1:
+ source = agent->config.software_version[1];
+ source_len = strnlen(agent->config.software_version[1],
+     sizeof(agent->config.software_version[1]));
+ break;
+ case OMCI_CONFIG_EQUIPMENT_ID:
+ source = agent->config.equipment_id;
+ source_len = strnlen(agent->config.equipment_id,
+     sizeof(agent->config.equipment_id));
+ break;
+ case OMCI_CONFIG_PASSWORD:
+ source = agent->config.password;
+ source_len = sizeof(agent->config.password);
+ break;
+ case OMCI_CONFIG_TRAFFIC_MGMT_OPTION:
+ scalar = agent->config.traffic_mgmt_option;
+ source = &scalar;
+ source_len = sizeof(scalar);
+ break;
+ case OMCI_CONFIG_ONU_TYPE:
+ scalar = agent->config.onu_type;
+ source = &scalar;
+ source_len = sizeof(scalar);
+ break;
+ case OMCI_CONFIG_UNI_COUNT:
+ scalar = agent->config.uni_count;
+ source = &scalar;
+ source_len = sizeof(scalar);
+ break;
+ case OMCI_CONFIG_AGENT_ENABLED:
+ scalar = agent->enabled;
+ source = &scalar;
+ source_len = sizeof(scalar);
+ break;
+ case OMCI_CONFIG_AGENT_PERMISSIVE:
+ scalar = agent->permissive;
+ source = &scalar;
+ source_len = sizeof(scalar);
+ break;
+ case OMCI_CONFIG_AGENT_FAKE_OMCI:
+ scalar = agent->fake_omci;
+ source = &scalar;
+ source_len = sizeof(scalar);
+ break;
+ case OMCI_CONFIG_AGENT_DYING_GASP:
+ scalar = agent->dying_gasp;
+ source = &scalar;
+ source_len = sizeof(scalar);
+ break;
+ case OMCI_CONFIG_OMCC_VERSION:
+ scalar = agent->config.omcc_version;
+ source = &scalar;
+ source_len = sizeof(scalar);
+ break;
+ case OMCI_CONFIG_OLT_PROFILE:
+ scalar = agent->config.olt_profile;
+ source = &scalar;
+ source_len = sizeof(scalar);
+ break;
+ case OMCI_CONFIG_OLT_PROFILE_FORCE:
+ scalar = agent->config.olt_profile_force;
+ source = &scalar;
+ source_len = sizeof(scalar);
+ break;
+ default:
+ mutex_unlock(&agent->lock);
+ return -EINVAL;
+ }
+
+ if (*len < source_len) {
+ *len = source_len;
+ mutex_unlock(&agent->lock);
+ return -ENOSPC;
+ }
+ memcpy(value, source, source_len);
+ *len = source_len;
+ mutex_unlock(&agent->lock);
+ return 0;
+}
+
+static int
+__omci_agent_config_set_source(struct omci_device *odev, u16 key,
+       const void *value, size_t len, u8 source,
+       bool notify)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_identity identity;
+ struct omci_olt_g *olt;
+ u8 normalized[OMCI_MAX_CONFIG_VALUE];
+ size_t normalized_len = sizeof(normalized);
+ u8 old_profile_source;
+ u8 old_force_source;
+ u8 old_profile;
+ u8 old_force;
+ u8 scalar;
+ bool profile_key = false;
+ bool operational_changed = false;
+ bool changed = false;
+ int ret = 0;
+
+ if ((key >= OMCI_CONFIG_SERIAL_NUMBER &&
+     key <= OMCI_CONFIG_PASSWORD) ||
+    key == OMCI_CONFIG_HARDWARE_VERSION ||
+    key == OMCI_CONFIG_SOFTWARE_VERSION_0 ||
+    key == OMCI_CONFIG_SOFTWARE_VERSION_1) {
+ ret = omci_identity_normalize_config(key, value, len,
+     normalized, &normalized_len);
+ if (ret)
+ return ret;
+ value = normalized;
+ len = normalized_len;
+ }
+
+ mutex_lock(&agent->lock);
+ old_profile = agent->config.olt_profile;
+ old_force = agent->config.olt_profile_force;
+ old_profile_source = agent->config.olt_profile_source;
+ old_force_source = agent->config.olt_profile_force_source;
+ switch (key) {
+ case OMCI_CONFIG_SERIAL_NUMBER:
+ if (len != sizeof(agent->config.serial_number)) {
+ ret = -EINVAL;
+ break;
+ }
+ changed = memcmp(agent->config.serial_number, value, len);
+ memcpy(agent->config.serial_number, value, len);
+ agent->config.serial_source = source;
+ memcpy(agent->config.vendor_id, value,
+       sizeof(agent->config.vendor_id));
+ agent->config.vendor_source = source;
+ break;
+ case OMCI_CONFIG_VENDOR_ID:
+ if (len != sizeof(agent->config.vendor_id)) {
+ ret = -EINVAL;
+ break;
+ }
+ changed = memcmp(agent->config.vendor_id, value, len);
+ memcpy(agent->config.vendor_id, value, len);
+ agent->config.vendor_source = source;
+ memcpy(agent->config.serial_number, value,
+       sizeof(agent->config.vendor_id));
+ agent->config.serial_source = max(agent->config.serial_source,
+  source);
+ break;
+ case OMCI_CONFIG_VERSION:
+ if (len != sizeof(agent->config.version)) {
+ ret = -EINVAL;
+ break;
+ }
+ changed = memcmp(agent->config.version, value, len) ||
+  memcmp(agent->config.software_version[0], value, len) ||
+  memcmp(agent->config.software_version[1], value, len);
+ memcpy(agent->config.version, value, len);
+ memcpy(agent->config.software_version[0], value, len);
+ memcpy(agent->config.software_version[1], value, len);
+ agent->config.version_source = source;
+ agent->config.software_version_source[0] = source;
+ agent->config.software_version_source[1] = source;
+ break;
+ case OMCI_CONFIG_HARDWARE_VERSION:
+ if (len != sizeof(agent->config.version)) {
+ ret = -EINVAL;
+ break;
+ }
+ changed = memcmp(agent->config.version, value, len);
+ memcpy(agent->config.version, value, len);
+ agent->config.version_source = source;
+ break;
+ case OMCI_CONFIG_SOFTWARE_VERSION_0:
+ case OMCI_CONFIG_SOFTWARE_VERSION_1: {
+ unsigned int image = key == OMCI_CONFIG_SOFTWARE_VERSION_0 ? 0 : 1;
+
+ if (len != sizeof(agent->config.software_version[image])) {
+ ret = -EINVAL;
+ break;
+ }
+ changed = memcmp(agent->config.software_version[image], value, len);
+ memcpy(agent->config.software_version[image], value, len);
+ agent->config.software_version_source[image] = source;
+ break;
+ }
+ case OMCI_CONFIG_EQUIPMENT_ID:
+ if (!len || len > sizeof(agent->config.equipment_id)) {
+ ret = -EINVAL;
+ break;
+ }
+ changed = memcmp(agent->config.equipment_id, value, len) ||
+  memchr_inv(agent->config.equipment_id + len, 0,
+     sizeof(agent->config.equipment_id) - len);
+ memset(agent->config.equipment_id, 0,
+       sizeof(agent->config.equipment_id));
+ memcpy(agent->config.equipment_id, value, len);
+ agent->config.equipment_source = source;
+ break;
+ case OMCI_CONFIG_PASSWORD:
+ if (len != sizeof(agent->config.password)) {
+ ret = -EINVAL;
+ break;
+ }
+ changed = memcmp(agent->config.password, value, len);
+ memcpy(agent->config.password, value, len);
+ agent->config.password_source = source;
+ break;
+ case OMCI_CONFIG_OLT_PROFILE:
+ if (len != sizeof(scalar)) {
+ ret = -EINVAL;
+ break;
+ }
+ scalar = *(const u8 *)value;
+ if (!omci_profile_valid(scalar)) {
+ ret = -EINVAL;
+ break;
+ }
+ changed = agent->config.olt_profile != scalar;
+ agent->config.olt_profile = scalar;
+ agent->config.olt_profile_source = source;
+ profile_key = true;
+ break;
+ case OMCI_CONFIG_OLT_PROFILE_FORCE:
+ if (len != sizeof(scalar)) {
+ ret = -EINVAL;
+ break;
+ }
+ scalar = *(const u8 *)value;
+ if (!omci_profile_forceable(scalar)) {
+ ret = -EINVAL;
+ break;
+ }
+ changed = agent->config.olt_profile_force != scalar;
+ agent->config.olt_profile_force = scalar;
+ agent->config.olt_profile_force_source = source;
+ profile_key = true;
+ break;
+ case OMCI_CONFIG_AGENT_DYING_GASP:
+ if (len != sizeof(scalar)) {
+ ret = -EINVAL;
+ break;
+ }
+ scalar = *(const u8 *)value;
+ changed = agent->dying_gasp != !!scalar;
+ agent->dying_gasp = !!scalar;
+ agent->config.dying_gasp_source = source;
+ omci_agent_reset_duplicate_locked(agent);
+ break;
+ case OMCI_CONFIG_OMCC_VERSION:
+ if (len != sizeof(scalar)) {
+ ret = -EINVAL;
+ break;
+ }
+ scalar = *(const u8 *)value;
+ if (!scalar) {
+ ret = -EINVAL;
+ break;
+ }
+ changed = agent->config.omcc_version != scalar;
+ agent->config.omcc_version = scalar;
+ agent->config.omcc_version_source = source;
+ break;
+ case OMCI_CONFIG_TRAFFIC_MGMT_OPTION:
+ case OMCI_CONFIG_ONU_TYPE:
+ case OMCI_CONFIG_UNI_COUNT:
+ case OMCI_CONFIG_AGENT_ENABLED:
+ case OMCI_CONFIG_AGENT_PERMISSIVE:
+ case OMCI_CONFIG_AGENT_FAKE_OMCI:
+ if (len != sizeof(scalar)) {
+ ret = -EINVAL;
+ break;
+ }
+ scalar = *(const u8 *)value;
+ if (key == OMCI_CONFIG_TRAFFIC_MGMT_OPTION) {
+ changed = agent->config.traffic_mgmt_option != scalar;
+ agent->config.traffic_mgmt_option = scalar;
+ } else if (key == OMCI_CONFIG_ONU_TYPE) {
+ if (scalar > OMCI_ONU_TYPE_CBU) {
+ ret = -EINVAL;
+ break;
+ }
+ changed = agent->config.onu_type != scalar;
+ agent->config.onu_type = scalar;
+ agent->config.onu_type_source = source;
+ } else if (key == OMCI_CONFIG_UNI_COUNT) {
+ scalar = clamp_t(u8, scalar, 1, 16);
+ changed = agent->config.uni_count != scalar;
+ agent->config.uni_count = scalar;
+ } else if (key == OMCI_CONFIG_AGENT_ENABLED) {
+ changed = agent->enabled != !!scalar;
+ agent->enabled = !!scalar;
+ if (!agent->enabled && agent->operational) {
+ agent->operational = false;
+ operational_changed = true;
+ }
+ } else if (key == OMCI_CONFIG_AGENT_PERMISSIVE) {
+ changed = agent->permissive != !!scalar;
+ agent->permissive = !!scalar;
+ } else {
+ changed = agent->fake_omci != !!scalar;
+ agent->fake_omci = !!scalar;
+ }
+ omci_agent_reset_duplicate_locked(agent);
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+ if (!ret && profile_key) {
+ olt = omci_agent_olt_g_locked(agent);
+ ret = omci_agent_profile_refresh_locked(odev, olt, NULL);
+ if (ret) {
+ agent->config.olt_profile = old_profile;
+ agent->config.olt_profile_force = old_force;
+ agent->config.olt_profile_source = old_profile_source;
+ agent->config.olt_profile_force_source = old_force_source;
+ }
+ }
+ if (!ret && changed && !profile_key) {
+ omci_agent_refresh_identity_locked(agent);
+ omci_agent_reset_duplicate_locked(agent);
+ omci_agent_reset_table_snapshot_locked(agent);
+ agent->upload_index = 0;
+ }
+ if (!ret && notify && changed)
+ omci_agent_identity_state_locked(agent, &identity);
+ mutex_unlock(&agent->lock);
+ if (operational_changed)
+ omci_agent_report_operational(odev, false);
+ if (!ret && notify && changed && odev->ops->config_changed)
+ odev->ops->config_changed(odev, key, &identity);
+ return ret;
+}
+
+int omci_agent_config_set_source(struct omci_device *odev, u16 key,
+ const void *value, size_t len, u8 source)
+{
+ return __omci_agent_config_set_source(odev, key, value, len, source,
+      false);
+}
+
+int omci_agent_config_set(struct omci_device *odev, u16 key,
+  const void *value, size_t len)
+{
+ return omci_agent_config_set_userspace(odev, key, value, len,
+       OMCI_CONFIG_SOURCE_NETLINK);
+}
+
+int omci_agent_config_set_userspace(struct omci_device *odev, u16 key,
+    const void *value, size_t len, u8 source)
+{
+ if (source != OMCI_CONFIG_SOURCE_NETLINK &&
+    source != OMCI_CONFIG_SOURCE_SYSFS)
+ return -EINVAL;
+
+ return __omci_agent_config_set_source(odev, key, value, len, source,
+      true);
+}
+
+int omci_agent_config_source_get(struct omci_device *odev, u16 key, u8 *source)
+{
+ struct omci_agent *agent = &odev->agent;
+ int ret = 0;
+
+ if (!source)
+ return -EINVAL;
+ mutex_lock(&agent->lock);
+ switch (key) {
+ case OMCI_CONFIG_SERIAL_NUMBER:
+ *source = agent->config.serial_source;
+ break;
+ case OMCI_CONFIG_VENDOR_ID:
+ *source = agent->config.vendor_source;
+ break;
+ case OMCI_CONFIG_VERSION:
+ case OMCI_CONFIG_HARDWARE_VERSION:
+ *source = agent->config.version_source;
+ break;
+ case OMCI_CONFIG_SOFTWARE_VERSION_0:
+ *source = agent->config.software_version_source[0];
+ break;
+ case OMCI_CONFIG_SOFTWARE_VERSION_1:
+ *source = agent->config.software_version_source[1];
+ break;
+ case OMCI_CONFIG_EQUIPMENT_ID:
+ *source = agent->config.equipment_source;
+ break;
+ case OMCI_CONFIG_PASSWORD:
+ *source = agent->config.password_source;
+ break;
+ case OMCI_CONFIG_ONU_TYPE:
+ *source = agent->config.onu_type_source;
+ break;
+ case OMCI_CONFIG_OLT_PROFILE:
+ *source = agent->config.olt_profile_source;
+ break;
+ case OMCI_CONFIG_OLT_PROFILE_FORCE:
+ *source = agent->config.olt_profile_force_source;
+ break;
+ case OMCI_CONFIG_AGENT_DYING_GASP:
+ *source = agent->config.dying_gasp_source;
+ break;
+ case OMCI_CONFIG_OMCC_VERSION:
+ *source = agent->config.omcc_version_source;
+ break;
+ default:
+ *source = OMCI_CONFIG_SOURCE_UNSPEC;
+ ret = -ENOENT;
+ break;
+ }
+ mutex_unlock(&agent->lock);
+ return ret;
+}
+
+int omci_agent_mib_get(struct omci_device *odev, u16 class_id, u16 entity_id,
+       struct omci_mib_object *object)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_mib_object *stored;
+ int ret = 0;
+
+ mutex_lock(&agent->lock);
+ stored = omci_mib_lookup(agent, class_id, entity_id);
+ if (!stored || !omci_mib_object_active(agent, stored))
+ ret = -ENOENT;
+ else
+ memcpy(object, stored, sizeof(*object));
+ mutex_unlock(&agent->lock);
+ return ret;
+}
+
+int omci_agent_mib_set(struct omci_device *odev,
+       const struct omci_mib_object *object)
+{
+ struct omci_agent *agent = &odev->agent;
+ const struct omci_me_desc *desc;
+ struct omci_mib_object *stored;
+ struct omci_mib_object *local;
+ bool datapath;
+ int ret;
+
+ local = kmemdup(object, sizeof(*local), GFP_KERNEL);
+ if (!local)
+ return -ENOMEM;
+
+ mutex_lock(&agent->lock);
+ desc = omci_me_lookup(agent, local->class_id);
+ if (!omci_me_active(agent, local->class_id)) {
+ ret = -ENOENT;
+ goto unlock;
+ }
+ local->origin = OMCI_MIB_ORIGIN_LOCAL;
+ local->owner_profile = desc && (desc->flags & OMCI_ME_F_VENDOR) ?
+ agent->profile_effective : OMCI_OLT_PROFILE_UNSPEC;
+ if (!local->data_len)
+ local->data_len = desc ? desc->data_len : sizeof(local->data);
+ if (local->data_len > sizeof(local->data)) {
+ ret = -E2BIG;
+ goto unlock;
+ }
+ if (local->class_id == OMCI_CLASS_OLT_G) {
+ ret = omci_olt_g_parse_set(local, local->attr_mask, local->data,
+   local->data_len);
+ if (ret)
+ goto unlock;
+ } else if (local->class_id == OMCI_CLASS_VLAN_TAGGING_FILTER_DATA) {
+ omci_vlan_filter_parse_create(local, local->data);
+ } else if (local->class_id == OMCI_CLASS_EXTENDED_VLAN) {
+ omci_ext_vlan_parse_create(local, local->data);
+ if (local->attr_mask & OMCI_EXT_VLAN_TABLE_MASK)
+ omci_ext_vlan_update_rule(&local->extended_vlan,
+  local->data);
+ }
+
+ ret = omci_mib_store_locked(agent, local);
+ if (ret)
+ goto unlock;
+ stored = omci_mib_lookup(agent, local->class_id, local->entity_id);
+ if (local->class_id == OMCI_CLASS_OLT_G)
+ ret = omci_agent_profile_refresh_locked(odev, &stored->olt_g,
+ NULL);
+ datapath = omci_agent_datapath_class(agent, local->class_id);
+ if (!ret && datapath)
+ ret = omci_agent_reconcile_services_locked(odev);
+ if (!ret)
+ omci_agent_reset_duplicate_locked(agent);
+
+unlock:
+ mutex_unlock(&agent->lock);
+ kfree(local);
+ return ret;
+}
+
+int omci_agent_mib_delete(struct omci_device *odev, u16 class_id,
+  u16 entity_id)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_mib_object *object;
+ bool datapath;
+ int ret = 0;
+
+ mutex_lock(&agent->lock);
+ object = xa_erase(&agent->mib, omci_mib_key(class_id, entity_id));
+ if (!object) {
+ ret = -ENOENT;
+ goto unlock;
+ }
+ if (class_id == OMCI_CLASS_OLT_G)
+ ret = omci_agent_profile_refresh_locked(odev, NULL, NULL);
+ datapath = omci_agent_datapath_class(agent, class_id);
+ if (!ret && datapath)
+ ret = omci_agent_reconcile_services_locked(odev);
+ if (!ret)
+ omci_agent_reset_duplicate_locked(agent);
+
+unlock:
+ mutex_unlock(&agent->lock);
+ kfree(object);
+ return ret;
+}
+
+void omci_agent_mib_reset(struct omci_device *odev, bool all)
+{
+ struct omci_agent *agent = &odev->agent;
+
+ mutex_lock(&agent->lock);
+ omci_agent_mib_reset_locked(odev, all, NULL);
+ mutex_unlock(&agent->lock);
+}
+
+int omci_agent_mib_next(struct omci_device *odev, u32 index,
+ struct omci_mib_object *object, u32 *next_index,
+ const char **name)
+{
+ struct omci_agent *agent = &odev->agent;
+ struct omci_mib_object *stored;
+ unsigned long xa_index = index;
+ int ret = 0;
+
+ mutex_lock(&agent->lock);
+ for (;;) {
+ stored = xa_find(&agent->mib, &xa_index, ULONG_MAX, XA_PRESENT);
+ if (!stored) {
+ ret = -ENOENT;
+ break;
+ }
+ if (omci_mib_object_active(agent, stored))
+ break;
+ xa_index++;
+ }
+ if (!ret) {
+ memcpy(object, stored, sizeof(*object));
+ *next_index = xa_index + 1;
+ *name = omci_me_class_name(stored->class_id);
+ }
+ mutex_unlock(&agent->lock);
+ return ret;
+}
+
+int omci_device_reconcile_services(struct omci_device *odev)
+{
+ struct omci_agent *agent;
+ int ret;
+
+ if (!odev)
+ return -EINVAL;
+
+ agent = &odev->agent;
+ mutex_lock(&agent->lock);
+ ret = omci_agent_reconcile_services_locked(odev);
+ mutex_unlock(&agent->lock);
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(omci_device_reconcile_services);
diff --git a/net/xpon/omci/core.c b/net/xpon/omci/core.c
new file mode 100644
index 000000000000..f97d43a9f0eb
--- /dev/null
+++ b/net/xpon/omci/core.c
@@ -0,0 +1,1691 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Generic ONU Management and Control Interface core
+ *
+ * The core owns the in-kernel baseline OMCI agent, the operational MIB and
+ * an optional Generic Netlink observer and administration endpoint.
+ */
+
+#include <linux/atomic.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/list.h>
+#include <linux/module.h>
+#include <linux/netdevice.h>
+#include <linux/netlink.h>
+#include <linux/notifier.h>
+#include <linux/slab.h>
+#include <linux/unaligned.h>
+#include <net/genetlink.h>
+#include <net/netlink.h>
+
+#include "../internal.h"
+#include "internal.h"
+
+static LIST_HEAD(omci_devices);
+static DEFINE_MUTEX(omci_devices_lock);
+static atomic_t omci_next_id = ATOMIC_INIT(0);
+static struct genl_family omci_genl_family;
+
+static const struct nla_policy omci_policy[OMCI_ATTR_MAX + 1] = {
+ [OMCI_ATTR_DEV_ID] = { .type = NLA_U32 },
+ [OMCI_ATTR_PDU] = {
+ .type = NLA_BINARY,
+ .len = OMCI_MAX_PDU_LEN,
+ },
+ [OMCI_ATTR_AGENT_ENABLED] = { .type = NLA_U8 },
+ [OMCI_ATTR_AGENT_PERMISSIVE] = { .type = NLA_U8 },
+ [OMCI_ATTR_AGENT_FAKE_OMCI] = { .type = NLA_U8 },
+ [OMCI_ATTR_AGENT_DYING_GASP] = { .type = NLA_U8 },
+ [OMCI_ATTR_ONU_TYPE] = { .type = NLA_U8 },
+ [OMCI_ATTR_CLASS_ID] = { .type = NLA_U16 },
+ [OMCI_ATTR_ENTITY_ID] = { .type = NLA_U16 },
+ [OMCI_ATTR_ATTR_MASK] = { .type = NLA_U16 },
+ [OMCI_ATTR_ATTR_DATA] = {
+ .type = NLA_BINARY,
+ .len = OMCI_MAX_ATTR_DATA,
+ },
+ [OMCI_ATTR_ORIGIN] = { .type = NLA_U8 },
+ [OMCI_ATTR_INDEX] = { .type = NLA_U32 },
+ [OMCI_ATTR_CONFIG_KEY] = { .type = NLA_U16 },
+ [OMCI_ATTR_CONFIG_VALUE] = {
+ .type = NLA_BINARY,
+ .len = OMCI_MAX_CONFIG_VALUE,
+ },
+ [OMCI_ATTR_VLAN_TAGGING_FILTER] = { .type = NLA_NESTED },
+ [OMCI_ATTR_EXTENDED_VLAN] = { .type = NLA_NESTED },
+ [OMCI_ATTR_CLASS_CATEGORY] = { .type = NLA_U8 },
+ [OMCI_ATTR_CLASS_SUPPORT] = { .type = NLA_U8 },
+ [OMCI_ATTR_CLASS_FLAGS] = { .type = NLA_U32 },
+ [OMCI_ATTR_CONFIG_SOURCE] = { .type = NLA_U8 },
+ [OMCI_ATTR_OLT_PROFILE_CONFIGURED] = { .type = NLA_U8 },
+ [OMCI_ATTR_OLT_PROFILE_FORCED] = { .type = NLA_U8 },
+};
+
+static struct omci_device *omci_find_locked(u32 id)
+{
+ struct omci_device *odev;
+
+ list_for_each_entry(odev, &omci_devices, list)
+ if (odev->id == id)
+ return odev;
+
+ return NULL;
+}
+
+static struct omci_device *omci_get_from_info(struct genl_info *info)
+{
+ u32 id = 0;
+
+ if (info->attrs[OMCI_ATTR_DEV_ID])
+ id = nla_get_u32(info->attrs[OMCI_ATTR_DEV_ID]);
+
+ return omci_find_locked(id);
+}
+
+static int omci_put_telemetry(struct sk_buff *msg, struct omci_device *odev)
+{
+ struct omci_telemetry telemetry = {};
+
+ if (odev->ops->get_telemetry)
+ odev->ops->get_telemetry(odev, &telemetry);
+
+ if (nla_put_u32(msg, OMCI_ATTR_TELEMETRY_VALID, telemetry.valid))
+ return -EMSGSIZE;
+
+ if ((telemetry.valid & OMCI_TELEMETRY_F_FEC_DOWNSTREAM) &&
+    nla_put_u8(msg, OMCI_ATTR_FEC_DOWNSTREAM,
+       telemetry.downstream_fec))
+ return -EMSGSIZE;
+ if ((telemetry.valid & OMCI_TELEMETRY_F_FEC_UPSTREAM) &&
+    nla_put_u8(msg, OMCI_ATTR_FEC_UPSTREAM, telemetry.upstream_fec))
+ return -EMSGSIZE;
+ if ((telemetry.valid & OMCI_TELEMETRY_F_BOSA_TEMPERATURE) &&
+    nla_put_s32(msg, OMCI_ATTR_BOSA_TEMPERATURE_MC,
+ telemetry.bosa_temperature_mc))
+ return -EMSGSIZE;
+ if ((telemetry.valid & OMCI_TELEMETRY_F_BOSA_VOLTAGE) &&
+    nla_put_u32(msg, OMCI_ATTR_BOSA_VOLTAGE_UV,
+ telemetry.bosa_voltage_uv))
+ return -EMSGSIZE;
+ if ((telemetry.valid & OMCI_TELEMETRY_F_BOSA_BIAS) &&
+    nla_put_u32(msg, OMCI_ATTR_BOSA_BIAS_UA, telemetry.bosa_bias_ua))
+ return -EMSGSIZE;
+ if ((telemetry.valid & OMCI_TELEMETRY_F_BOSA_TX_POWER) &&
+    nla_put_u32(msg, OMCI_ATTR_BOSA_TX_POWER_NW,
+ telemetry.bosa_tx_power_nw))
+ return -EMSGSIZE;
+ if ((telemetry.valid & OMCI_TELEMETRY_F_BOSA_RX_POWER) &&
+    nla_put_u32(msg, OMCI_ATTR_BOSA_RX_POWER_NW,
+ telemetry.bosa_rx_power_nw))
+ return -EMSGSIZE;
+ if ((telemetry.valid & OMCI_TELEMETRY_F_BOSA_ALARMS) &&
+    nla_put_u32(msg, OMCI_ATTR_BOSA_ALARMS, telemetry.bosa_alarms))
+ return -EMSGSIZE;
+
+ return 0;
+}
+
+static int omci_put_status(struct sk_buff *msg, struct omci_device *odev)
+{
+ u32 owner_portid;
+ u32 flags = 0;
+ u16 onu_id;
+ u16 gem_port_id;
+ u8 state;
+
+ mutex_lock(&odev->owner_lock);
+ owner_portid = odev->owner_portid;
+ mutex_unlock(&odev->owner_lock);
+
+ spin_lock_bh(&odev->state_lock);
+ onu_id = odev->onu_id;
+ gem_port_id = odev->gem_port_id;
+ state = odev->state;
+ if (odev->channel_up)
+ flags |= OMCI_F_CHANNEL_UP;
+ spin_unlock_bh(&odev->state_lock);
+
+ if (nla_put_u32(msg, OMCI_ATTR_DEV_ID, odev->id) ||
+    nla_put_u32(msg, OMCI_ATTR_IFINDEX, odev->ifindex) ||
+    nla_put_u16(msg, OMCI_ATTR_ONU_ID, onu_id) ||
+    nla_put_u16(msg, OMCI_ATTR_GEM_PORT_ID, gem_port_id) ||
+    nla_put_u8(msg, OMCI_ATTR_STATE, state) ||
+    nla_put_u32(msg, OMCI_ATTR_FLAGS, flags) ||
+    nla_put_u32(msg, OMCI_ATTR_CAPABILITIES, odev->capabilities) ||
+    nla_put_u32(msg, OMCI_ATTR_OWNER_PORTID, owner_portid) ||
+    nla_put_u64_64bit(msg, OMCI_ATTR_RX_PACKETS,
+      atomic64_read(&odev->rx_packets), OMCI_ATTR_PAD) ||
+    nla_put_u64_64bit(msg, OMCI_ATTR_RX_BYTES,
+      atomic64_read(&odev->rx_bytes), OMCI_ATTR_PAD) ||
+    nla_put_u64_64bit(msg, OMCI_ATTR_RX_DROPPED,
+      atomic64_read(&odev->rx_dropped), OMCI_ATTR_PAD) ||
+    nla_put_u64_64bit(msg, OMCI_ATTR_TX_PACKETS,
+      atomic64_read(&odev->tx_packets), OMCI_ATTR_PAD) ||
+    nla_put_u64_64bit(msg, OMCI_ATTR_TX_BYTES,
+      atomic64_read(&odev->tx_bytes), OMCI_ATTR_PAD) ||
+    nla_put_u64_64bit(msg, OMCI_ATTR_TX_ERRORS,
+      atomic64_read(&odev->tx_errors), OMCI_ATTR_PAD))
+ return -EMSGSIZE;
+
+ if (omci_put_telemetry(msg, odev))
+ return -EMSGSIZE;
+
+ return omci_agent_put_status(msg, odev);
+}
+
+static int omci_cmd_get(struct sk_buff *skb, struct genl_info *info)
+{
+ struct omci_device *odev;
+ struct sk_buff *msg;
+ void *hdr;
+ int ret;
+
+ mutex_lock(&omci_devices_lock);
+ odev = omci_get_from_info(info);
+ if (!odev) {
+ ret = -ENODEV;
+ goto out_unlock;
+ }
+
+ msg = genlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg) {
+ ret = -ENOMEM;
+ goto out_unlock;
+ }
+
+ hdr = genlmsg_put_reply(msg, info, &omci_genl_family, 0,
+ OMCI_CMD_GET);
+ if (!hdr) {
+ ret = -EMSGSIZE;
+ goto free_msg;
+ }
+
+ ret = omci_put_status(msg, odev);
+ if (ret)
+ goto cancel_msg;
+
+ genlmsg_end(msg, hdr);
+ mutex_unlock(&omci_devices_lock);
+ return genlmsg_reply(msg, info);
+
+cancel_msg:
+ genlmsg_cancel(msg, hdr);
+free_msg:
+ nlmsg_free(msg);
+out_unlock:
+ mutex_unlock(&omci_devices_lock);
+ return ret;
+}
+
+static int omci_cmd_bind(struct sk_buff *skb, struct genl_info *info)
+{
+ struct net *net = genl_info_net(info);
+ struct omci_device *odev;
+ int ret = 0;
+
+ mutex_lock(&omci_devices_lock);
+ odev = omci_get_from_info(info);
+ if (!odev) {
+ ret = -ENODEV;
+ goto out_unlock_devices;
+ }
+
+ mutex_lock(&odev->owner_lock);
+ if (odev->owner_portid &&
+    (odev->owner_portid != info->snd_portid ||
+     odev->owner_net != net)) {
+ ret = -EBUSY;
+ goto out_unlock_owner;
+ }
+
+ if (!odev->owner_portid) {
+ odev->owner_net = get_net(net);
+ odev->owner_portid = info->snd_portid;
+ schedule_work(&odev->rx_work);
+ }
+
+out_unlock_owner:
+ mutex_unlock(&odev->owner_lock);
+out_unlock_devices:
+ mutex_unlock(&omci_devices_lock);
+ return ret;
+}
+
+static int omci_cmd_unbind(struct sk_buff *skb, struct genl_info *info)
+{
+ struct omci_device *odev;
+ struct net *owner_net = NULL;
+ int ret = 0;
+
+ mutex_lock(&omci_devices_lock);
+ odev = omci_get_from_info(info);
+ if (!odev) {
+ ret = -ENODEV;
+ goto out_unlock_devices;
+ }
+
+ mutex_lock(&odev->owner_lock);
+ if (odev->owner_portid != info->snd_portid ||
+    odev->owner_net != genl_info_net(info)) {
+ ret = -EPERM;
+ goto out_unlock_owner;
+ }
+
+ owner_net = odev->owner_net;
+ odev->owner_net = NULL;
+ odev->owner_portid = 0;
+
+out_unlock_owner:
+ mutex_unlock(&odev->owner_lock);
+out_unlock_devices:
+ mutex_unlock(&omci_devices_lock);
+ if (owner_net)
+ put_net(owner_net);
+ return ret;
+}
+
+int omci_validate_tx(struct omci_device *odev, struct sk_buff *skb)
+{
+ const u8 *data = skb->data;
+ u16 content_len;
+ u16 pdu_len;
+
+ if (skb->len < 4)
+ return -EMSGSIZE;
+
+ switch (data[3]) {
+ case OMCI_BASELINE_DEV_ID:
+ if (skb->len != OMCI_BASELINE_LEN_NO_MIC &&
+    skb->len != OMCI_BASELINE_LEN)
+ return -EMSGSIZE;
+ if ((odev->capabilities & OMCI_CAP_HW_MIC) &&
+    skb->len == OMCI_BASELINE_LEN)
+ skb_trim(skb, OMCI_BASELINE_LEN_NO_MIC);
+ break;
+ case OMCI_EXTENDED_DEV_ID:
+ if (skb->len < OMCI_EXTENDED_HEADER_LEN)
+ return -EMSGSIZE;
+ content_len = get_unaligned_be16(data + 8);
+ pdu_len = OMCI_EXTENDED_HEADER_LEN + content_len;
+ if (pdu_len > OMCI_MAX_PDU_LEN || skb->len < pdu_len)
+ return -EMSGSIZE;
+ if (skb->len > pdu_len) {
+ if (skb->len - pdu_len != OMCI_MIC_LEN)
+ return -EMSGSIZE;
+ if (odev->capabilities & OMCI_CAP_HW_MIC)
+ skb_trim(skb, pdu_len);
+ }
+ break;
+ default:
+ return -EPROTO;
+ }
+
+ return 0;
+}
+
+int omci_device_xmit(struct omci_device *odev, const void *data, size_t len)
+{
+ struct sk_buff *tx_skb;
+ u16 gem_port_id;
+ bool channel_up;
+ u32 tx_len;
+ int ret;
+
+ if (!READ_ONCE(odev->started))
+ return -ENETDOWN;
+
+ spin_lock_bh(&odev->state_lock);
+ gem_port_id = odev->gem_port_id;
+ channel_up = odev->channel_up;
+ spin_unlock_bh(&odev->state_lock);
+ if (!channel_up)
+ return -ENOLINK;
+
+ tx_skb = alloc_skb(len, GFP_KERNEL);
+ if (!tx_skb)
+ return -ENOMEM;
+ skb_put_data(tx_skb, data, len);
+ ret = omci_validate_tx(odev, tx_skb);
+ if (ret)
+ goto free_skb;
+
+ tx_len = tx_skb->len;
+ ret = odev->ops->xmit(odev, tx_skb, gem_port_id);
+ if (ret)
+ goto free_skb;
+
+ atomic64_inc(&odev->tx_packets);
+ atomic64_add(tx_len, &odev->tx_bytes);
+ return 0;
+
+free_skb:
+ atomic64_inc(&odev->tx_errors);
+ dev_kfree_skb_any(tx_skb);
+ return ret;
+}
+
+static int omci_cmd_tx(struct sk_buff *skb, struct genl_info *info)
+{
+ struct nlattr *pdu_attr = info->attrs[OMCI_ATTR_PDU];
+ struct omci_device *odev;
+ struct sk_buff *tx_skb;
+ u16 gem_port_id;
+ bool channel_up;
+ u32 tx_len;
+ int ret;
+
+ if (!pdu_attr)
+ return -EINVAL;
+
+ mutex_lock(&omci_devices_lock);
+ odev = omci_get_from_info(info);
+ if (!odev) {
+ ret = -ENODEV;
+ goto out_unlock_devices;
+ }
+
+ mutex_lock(&odev->owner_lock);
+ if (odev->owner_portid != info->snd_portid ||
+    odev->owner_net != genl_info_net(info)) {
+ ret = -EPERM;
+ goto out_unlock_owner;
+ }
+ mutex_unlock(&odev->owner_lock);
+
+ if (!READ_ONCE(odev->started)) {
+ ret = -ENETDOWN;
+ goto out_unlock_devices;
+ }
+
+ spin_lock_bh(&odev->state_lock);
+ gem_port_id = odev->gem_port_id;
+ channel_up = odev->channel_up;
+ spin_unlock_bh(&odev->state_lock);
+ if (!channel_up) {
+ ret = -ENOLINK;
+ goto out_unlock_devices;
+ }
+
+ tx_skb = alloc_skb(nla_len(pdu_attr), GFP_KERNEL);
+ if (!tx_skb) {
+ ret = -ENOMEM;
+ goto out_unlock_devices;
+ }
+
+ skb_put_data(tx_skb, nla_data(pdu_attr), nla_len(pdu_attr));
+ ret = omci_validate_tx(odev, tx_skb);
+ if (ret)
+ goto free_tx_skb;
+
+ tx_len = tx_skb->len;
+ ret = odev->ops->xmit(odev, tx_skb, gem_port_id);
+ if (ret)
+ goto free_tx_skb;
+
+ atomic64_inc(&odev->tx_packets);
+ atomic64_add(tx_len, &odev->tx_bytes);
+ mutex_unlock(&omci_devices_lock);
+ return 0;
+
+free_tx_skb:
+ atomic64_inc(&odev->tx_errors);
+ dev_kfree_skb_any(tx_skb);
+ goto out_unlock_devices;
+
+out_unlock_owner:
+ mutex_unlock(&odev->owner_lock);
+out_unlock_devices:
+ mutex_unlock(&omci_devices_lock);
+ return ret;
+}
+
+static int omci_put_olt_g(struct sk_buff *msg,
+  const struct omci_mib_object *object)
+{
+ const struct omci_olt_g *olt = &object->olt_g;
+ struct nlattr *nested;
+
+ if (object->class_id != OMCI_CLASS_OLT_G || !olt->valid)
+ return 0;
+
+ nested = nla_nest_start(msg, OMCI_ATTR_OLT_G);
+ if (!nested)
+ return -EMSGSIZE;
+ if ((olt->vendor_id_valid &&
+     nla_put_string(msg, OMCI_OLT_G_ATTR_VENDOR_ID, olt->vendor_id)) ||
+    (olt->equipment_id_valid &&
+     nla_put_string(msg, OMCI_OLT_G_ATTR_EQUIPMENT_ID,
+    olt->equipment_id)) ||
+    (olt->version_valid &&
+     nla_put_string(msg, OMCI_OLT_G_ATTR_VERSION, olt->version))) {
+ nla_nest_cancel(msg, nested);
+ return -EMSGSIZE;
+ }
+ nla_nest_end(msg, nested);
+
+ return 0;
+}
+
+static int omci_put_vlan_filter(struct sk_buff *msg,
+ const struct omci_mib_object *object)
+{
+ const struct omci_vlan_tagging_filter *filter = &object->vlan_filter;
+ struct nlattr *entries;
+ struct nlattr *nested;
+ unsigned int i;
+
+ if (object->class_id != OMCI_CLASS_VLAN_TAGGING_FILTER_DATA || !filter->valid)
+ return 0;
+
+ nested = nla_nest_start(msg, OMCI_ATTR_VLAN_TAGGING_FILTER);
+ if (!nested)
+ return -EMSGSIZE;
+ if (nla_put_u8(msg, OMCI_VLAN_FILTER_ATTR_FORWARD_OPERATION,
+       filter->forward_operation) ||
+    nla_put_u8(msg, OMCI_VLAN_FILTER_ATTR_NUMBER_OF_ENTRIES,
+       filter->num_entries))
+ goto cancel;
+
+ entries = nla_nest_start(msg, OMCI_VLAN_FILTER_ATTR_ENTRIES);
+ if (!entries)
+ goto cancel;
+ for (i = 0; i < filter->num_entries; i++) {
+ const struct omci_vlan_filter_entry *entry = &filter->entries[i];
+ struct nlattr *item;
+
+ item = nla_nest_start(msg, i + 1);
+ if (!item)
+ goto cancel_entries;
+ if (nla_put_u8(msg, OMCI_VLAN_FILTER_ENTRY_ATTR_INDEX, i) ||
+    nla_put_u16(msg, OMCI_VLAN_FILTER_ENTRY_ATTR_TCI,
+ entry->tci) ||
+    nla_put_u8(msg, OMCI_VLAN_FILTER_ENTRY_ATTR_PBIT,
+       entry->pbit) ||
+    nla_put_u8(msg, OMCI_VLAN_FILTER_ENTRY_ATTR_DEI,
+       entry->dei) ||
+    nla_put_u16(msg, OMCI_VLAN_FILTER_ENTRY_ATTR_VID,
+ entry->vid)) {
+ nla_nest_cancel(msg, item);
+ goto cancel_entries;
+ }
+ nla_nest_end(msg, item);
+ }
+ nla_nest_end(msg, entries);
+ nla_nest_end(msg, nested);
+
+ return 0;
+
+cancel_entries:
+ nla_nest_cancel(msg, entries);
+cancel:
+ nla_nest_cancel(msg, nested);
+ return -EMSGSIZE;
+}
+
+static int omci_put_ext_vlan_rule(struct sk_buff *msg,
+  const struct omci_extended_vlan_rule *rule,
+  unsigned int index)
+{
+ struct nlattr *nested;
+
+ nested = nla_nest_start(msg, index + 1);
+ if (!nested)
+ return -EMSGSIZE;
+ if (nla_put_u8(msg, OMCI_EXT_VLAN_RULE_ATTR_INDEX, index) ||
+    nla_put(msg, OMCI_EXT_VLAN_RULE_ATTR_RAW, sizeof(rule->raw),
+    rule->raw) ||
+    nla_put_u8(msg, OMCI_EXT_VLAN_RULE_ATTR_DELETE, rule->delete) ||
+    nla_put_u8(msg, OMCI_EXT_VLAN_RULE_ATTR_FILTER_OUTER_PBIT,
+       rule->filter_outer_pbit) ||
+    nla_put_u16(msg, OMCI_EXT_VLAN_RULE_ATTR_FILTER_OUTER_VID,
rule->filter_outer_vid) ||
+    nla_put_u8(msg, OMCI_EXT_VLAN_RULE_ATTR_FILTER_OUTER_TPID_DEI,
+       rule->filter_outer_tpid_dei) ||
+    nla_put_u8(msg, OMCI_EXT_VLAN_RULE_ATTR_FILTER_INNER_PBIT,
+       rule->filter_inner_pbit) ||
+    nla_put_u16(msg, OMCI_EXT_VLAN_RULE_ATTR_FILTER_INNER_VID,
rule->filter_inner_vid) ||
+    nla_put_u8(msg, OMCI_EXT_VLAN_RULE_ATTR_FILTER_INNER_TPID_DEI,
+       rule->filter_inner_tpid_dei) ||
+    nla_put_u8(msg, OMCI_EXT_VLAN_RULE_ATTR_FILTER_ETHERTYPE,
+       rule->filter_ethertype) ||
+    nla_put_u8(msg, OMCI_EXT_VLAN_RULE_ATTR_TAGS_TO_REMOVE,
+       rule->tags_to_remove) ||
+    nla_put_u8(msg, OMCI_EXT_VLAN_RULE_ATTR_TREAT_OUTER_PBIT,
+       rule->treat_outer_pbit) ||
+    nla_put_u16(msg, OMCI_EXT_VLAN_RULE_ATTR_TREAT_OUTER_VID,
rule->treat_outer_vid) ||
+    nla_put_u8(msg, OMCI_EXT_VLAN_RULE_ATTR_TREAT_OUTER_TPID_DEI,
+       rule->treat_outer_tpid_dei) ||
+    nla_put_u8(msg, OMCI_EXT_VLAN_RULE_ATTR_TREAT_INNER_PBIT,
+       rule->treat_inner_pbit) ||
+    nla_put_u16(msg, OMCI_EXT_VLAN_RULE_ATTR_TREAT_INNER_VID,
rule->treat_inner_vid) ||
+    nla_put_u8(msg, OMCI_EXT_VLAN_RULE_ATTR_TREAT_INNER_TPID_DEI,
+       rule->treat_inner_tpid_dei)) {
+ nla_nest_cancel(msg, nested);
+ return -EMSGSIZE;
+ }
+ nla_nest_end(msg, nested);
+
+ return 0;
+}
+
+static int omci_put_extended_vlan(struct sk_buff *msg,
+  const struct omci_mib_object *object)
+{
+ const struct omci_extended_vlan *vlan = &object->extended_vlan;
+ struct nlattr *rules;
+ struct nlattr *nested;
+ unsigned int i;
+
+ if (object->class_id != OMCI_CLASS_EXTENDED_VLAN || !vlan->valid)
+ return 0;
+
+ nested = nla_nest_start(msg, OMCI_ATTR_EXTENDED_VLAN);
+ if (!nested)
+ return -EMSGSIZE;
+ if (nla_put_u8(msg, OMCI_EXT_VLAN_ATTR_ASSOCIATION_TYPE,
+       vlan->association_type) ||
+    nla_put_u16(msg, OMCI_EXT_VLAN_ATTR_MAX_TABLE_SIZE,
vlan->max_table_size) ||
+    nla_put_u16(msg, OMCI_EXT_VLAN_ATTR_INPUT_TPID, vlan->input_tpid) ||
+    nla_put_u16(msg, OMCI_EXT_VLAN_ATTR_OUTPUT_TPID, vlan->output_tpid) ||
+    nla_put_u8(msg, OMCI_EXT_VLAN_ATTR_DOWNSTREAM_MODE,
+       vlan->downstream_mode) ||
+    nla_put_u16(msg, OMCI_EXT_VLAN_ATTR_ASSOCIATED_ME, vlan->associated_me) ||
+    nla_put(msg, OMCI_EXT_VLAN_ATTR_DSCP_TO_PBIT,
+    sizeof(vlan->dscp_to_pbit), vlan->dscp_to_pbit))
+ goto cancel;
+
+ rules = nla_nest_start(msg, OMCI_EXT_VLAN_ATTR_RULES);
+ if (!rules)
+ goto cancel;
+ for (i = 0; i < vlan->rule_count; i++)
+ if (omci_put_ext_vlan_rule(msg, &vlan->rules[i], i))
+ goto cancel_rules;
+ nla_nest_end(msg, rules);
+ nla_nest_end(msg, nested);
+
+ return 0;
+
+cancel_rules:
+ nla_nest_cancel(msg, rules);
+cancel:
+ nla_nest_cancel(msg, nested);
+ return -EMSGSIZE;
+}
+
+static int omci_put_mib_object(struct sk_buff *msg,
+       const struct omci_mib_object *object,
+       u32 next_index, const char *name)
+{
+ if (nla_put_u16(msg, OMCI_ATTR_CLASS_ID, object->class_id) ||
+    nla_put_u16(msg, OMCI_ATTR_ENTITY_ID, object->entity_id) ||
+    nla_put_u16(msg, OMCI_ATTR_ATTR_MASK, object->attr_mask) ||
+    nla_put(msg, OMCI_ATTR_ATTR_DATA, sizeof(object->data),
+    object->data) ||
+    nla_put_u8(msg, OMCI_ATTR_ORIGIN, object->origin) ||
+    nla_put_u32(msg, OMCI_ATTR_INDEX, next_index) ||
+    (name && nla_put_string(msg, OMCI_ATTR_NAME, name)))
+ return -EMSGSIZE;
+ if (omci_put_olt_g(msg, object) ||
+    omci_put_vlan_filter(msg, object) ||
+    omci_put_extended_vlan(msg, object))
+ return -EMSGSIZE;
+ return 0;
+}
+
+static int omci_reply_mib(struct genl_info *info, u8 command,
+  struct omci_device *odev,
+  const struct omci_mib_object *object,
+  u32 next_index, const char *name)
+{
+ struct sk_buff *msg;
+ void *hdr;
+ int ret;
+
+ msg = genlmsg_new(NLMSG_GOODSIZE, GFP_KERNEL);
+ if (!msg)
+ return -ENOMEM;
+ hdr = genlmsg_put_reply(msg, info, &omci_genl_family, 0, command);
+ if (!hdr) {
+ nlmsg_free(msg);
+ return -EMSGSIZE;
+ }
+ if (nla_put_u32(msg, OMCI_ATTR_DEV_ID, odev->id)) {
+ ret = -EMSGSIZE;
+ goto cancel;
+ }
+ ret = omci_put_mib_object(msg, object, next_index, name);
+ if (ret)
+ goto cancel;
+ genlmsg_end(msg, hdr);
+ return genlmsg_reply(msg, info);
+
+cancel:
+ genlmsg_cancel(msg, hdr);
+ nlmsg_free(msg);
+ return ret;
+}
+
+static int omci_reply_class(struct genl_info *info, u8 command,
+    const struct omci_me_class *class, u32 next_index)
+{
+ struct sk_buff *msg;
+ void *hdr;
+
+ msg = genlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg)
+ return -ENOMEM;
+ hdr = genlmsg_put_reply(msg, info, &omci_genl_family, 0, command);
+ if (!hdr)
+ goto message_too_large;
+ if (nla_put_u16(msg, OMCI_ATTR_CLASS_ID, class->class_id) ||
+    nla_put_string(msg, OMCI_ATTR_NAME, class->name) ||
+    nla_put_u8(msg, OMCI_ATTR_CLASS_CATEGORY, class->category) ||
+    nla_put_u8(msg, OMCI_ATTR_CLASS_SUPPORT, class->support) ||
+    nla_put_u32(msg, OMCI_ATTR_CLASS_FLAGS, class->flags) ||
+    nla_put_u32(msg, OMCI_ATTR_INDEX, next_index))
+ goto cancel;
+
+ genlmsg_end(msg, hdr);
+ return genlmsg_reply(msg, info);
+
+cancel:
+ genlmsg_cancel(msg, hdr);
+message_too_large:
+ nlmsg_free(msg);
+ return -EMSGSIZE;
+}
+
+static int omci_cmd_class_get(struct sk_buff *skb, struct genl_info *info)
+{
+ struct omci_me_class class;
+ struct omci_device *odev;
+ u16 class_id;
+ int ret;
+
+ if (!info->attrs[OMCI_ATTR_CLASS_ID])
+ return -EINVAL;
+ class_id = nla_get_u16(info->attrs[OMCI_ATTR_CLASS_ID]);
+
+ mutex_lock(&omci_devices_lock);
+ odev = omci_get_from_info(info);
+ if (!odev) {
+ ret = -ENODEV;
+ goto out;
+ }
+ ret = omci_me_class_get(odev, class_id, &class);
+ if (!ret)
+ ret = omci_reply_class(info, OMCI_CMD_CLASS_GET, &class, 0);
+out:
+ mutex_unlock(&omci_devices_lock);
+ return ret;
+}
+
+static int omci_cmd_class_next(struct sk_buff *skb, struct genl_info *info)
+{
+ struct omci_me_class class;
+ struct omci_device *odev;
+ u32 index = 0;
+ u32 next_index;
+ int ret;
+
+ if (info->attrs[OMCI_ATTR_INDEX])
+ index = nla_get_u32(info->attrs[OMCI_ATTR_INDEX]);
+
+ mutex_lock(&omci_devices_lock);
+ odev = omci_get_from_info(info);
+ if (!odev) {
+ ret = -ENODEV;
+ goto out;
+ }
+ ret = omci_me_class_next(odev, index, &class, &next_index);
+ if (!ret)
+ ret = omci_reply_class(info, OMCI_CMD_CLASS_NEXT, &class,
+       next_index);
+out:
+ mutex_unlock(&omci_devices_lock);
+ return ret;
+}
+
+static int omci_cmd_agent_set(struct sk_buff *skb, struct genl_info *info)
+{
+ struct omci_device *odev;
+ bool profile_changed = false;
+ u8 value;
+ int ret = 0;
+
+ mutex_lock(&omci_devices_lock);
+ odev = omci_get_from_info(info);
+ if (!odev) {
+ ret = -ENODEV;
+ goto out;
+ }
+ if (info->attrs[OMCI_ATTR_AGENT_ENABLED]) {
+ value = nla_get_u8(info->attrs[OMCI_ATTR_AGENT_ENABLED]);
+ ret = omci_agent_config_set(odev, OMCI_CONFIG_AGENT_ENABLED,
+    &value, sizeof(value));
+ if (ret)
+ goto out;
+ }
+ if (info->attrs[OMCI_ATTR_AGENT_PERMISSIVE]) {
+ value = nla_get_u8(info->attrs[OMCI_ATTR_AGENT_PERMISSIVE]);
+ ret = omci_agent_config_set(odev, OMCI_CONFIG_AGENT_PERMISSIVE,
+    &value, sizeof(value));
+ if (ret)
+ goto out;
+ }
+ if (info->attrs[OMCI_ATTR_AGENT_FAKE_OMCI]) {
+ value = nla_get_u8(info->attrs[OMCI_ATTR_AGENT_FAKE_OMCI]);
+ ret = omci_agent_config_set(odev, OMCI_CONFIG_AGENT_FAKE_OMCI,
+    &value, sizeof(value));
+ if (ret)
+ goto out;
+ }
+ if (info->attrs[OMCI_ATTR_AGENT_DYING_GASP]) {
+ value = nla_get_u8(info->attrs[OMCI_ATTR_AGENT_DYING_GASP]);
+ ret = omci_agent_config_set(odev,
+    OMCI_CONFIG_AGENT_DYING_GASP,
+    &value, sizeof(value));
+ if (ret)
+ goto out;
+ }
+ if (info->attrs[OMCI_ATTR_OLT_PROFILE_CONFIGURED]) {
+ value = nla_get_u8(info->attrs[OMCI_ATTR_OLT_PROFILE_CONFIGURED]);
+ ret = omci_agent_config_set(odev, OMCI_CONFIG_OLT_PROFILE,
+    &value, sizeof(value));
+ if (ret)
+ goto out;
+ profile_changed = true;
+ }
+ if (info->attrs[OMCI_ATTR_OLT_PROFILE_FORCED]) {
+ value = nla_get_u8(info->attrs[OMCI_ATTR_OLT_PROFILE_FORCED]);
+ ret = omci_agent_config_set(odev, OMCI_CONFIG_OLT_PROFILE_FORCE,
+    &value, sizeof(value));
+ if (ret)
+ goto out;
+ profile_changed = true;
+ }
+ if (!ret)
+ omci_device_notify(odev, profile_changed ?
+   OMCI_EVENT_PROFILE_CHANGE :
+   OMCI_EVENT_CONFIG_CHANGE);
+out:
+ mutex_unlock(&omci_devices_lock);
+ return ret;
+}
+
+static int omci_cmd_config_get(struct sk_buff *skb, struct genl_info *info)
+{
+ u8 value[OMCI_MAX_CONFIG_VALUE];
+ struct omci_device *odev;
+ struct sk_buff *msg;
+ size_t len = sizeof(value);
+ u16 key;
+ u8 source = OMCI_CONFIG_SOURCE_UNSPEC;
+ void *hdr;
+ int ret;
+
+ if (!info->attrs[OMCI_ATTR_CONFIG_KEY])
+ return -EINVAL;
+ key = nla_get_u16(info->attrs[OMCI_ATTR_CONFIG_KEY]);
+
+ mutex_lock(&omci_devices_lock);
+ odev = omci_get_from_info(info);
+ if (!odev) {
+ ret = -ENODEV;
+ goto out_unlock;
+ }
+ ret = omci_agent_config_get(odev, key, value, &len);
+ if (ret)
+ goto out_unlock;
+ omci_agent_config_source_get(odev, key, &source);
+
+ msg = genlmsg_new(NLMSG_DEFAULT_SIZE + len, GFP_KERNEL);
+ if (!msg) {
+ ret = -ENOMEM;
+ goto out_unlock;
+ }
+ hdr = genlmsg_put_reply(msg, info, &omci_genl_family, 0,
+ OMCI_CMD_CONFIG_GET);
+ if (!hdr || nla_put_u32(msg, OMCI_ATTR_DEV_ID, odev->id) ||
+    nla_put_u16(msg, OMCI_ATTR_CONFIG_KEY, key) ||
+    nla_put(msg, OMCI_ATTR_CONFIG_VALUE, len, value) ||
+    nla_put_u8(msg, OMCI_ATTR_CONFIG_SOURCE, source)) {
+ nlmsg_free(msg);
+ ret = -EMSGSIZE;
+ goto out_unlock;
+ }
+ genlmsg_end(msg, hdr);
+ mutex_unlock(&omci_devices_lock);
+ return genlmsg_reply(msg, info);
+
+out_unlock:
+ mutex_unlock(&omci_devices_lock);
+ return ret;
+}
+
+static int omci_cmd_config_set(struct sk_buff *skb, struct genl_info *info)
+{
+ struct nlattr *value = info->attrs[OMCI_ATTR_CONFIG_VALUE];
+ struct omci_device *odev;
+ u16 key;
+ int ret;
+
+ if (!info->attrs[OMCI_ATTR_CONFIG_KEY] || !value)
+ return -EINVAL;
+ key = nla_get_u16(info->attrs[OMCI_ATTR_CONFIG_KEY]);
+
+ mutex_lock(&omci_devices_lock);
+ odev = omci_get_from_info(info);
+ if (!odev) {
+ ret = -ENODEV;
+ goto out;
+ }
+ ret = omci_agent_config_set(odev, key, nla_data(value),
+    nla_len(value));
+ if (!ret) {
+ u8 event = OMCI_EVENT_CONFIG_CHANGE;
+
+ if (key == OMCI_CONFIG_OLT_PROFILE ||
+    key == OMCI_CONFIG_OLT_PROFILE_FORCE)
+ event = OMCI_EVENT_PROFILE_CHANGE;
+ omci_device_notify(odev, event);
+ }
+out:
+ mutex_unlock(&omci_devices_lock);
+ return ret;
+}
+
+static int omci_cmd_mib_get(struct sk_buff *skb, struct genl_info *info)
+{
+ struct omci_mib_object *object;
+ struct omci_device *odev;
+ u16 class_id;
+ u16 entity_id;
+ int ret;
+
+ if (!info->attrs[OMCI_ATTR_CLASS_ID] ||
+    !info->attrs[OMCI_ATTR_ENTITY_ID])
+ return -EINVAL;
+ class_id = nla_get_u16(info->attrs[OMCI_ATTR_CLASS_ID]);
+ entity_id = nla_get_u16(info->attrs[OMCI_ATTR_ENTITY_ID]);
+
+ object = kzalloc(sizeof(*object), GFP_KERNEL);
+ if (!object)
+ return -ENOMEM;
+
+ mutex_lock(&omci_devices_lock);
+ odev = omci_get_from_info(info);
+ if (!odev) {
+ ret = -ENODEV;
+ goto out;
+ }
+ ret = omci_agent_mib_get(odev, class_id, entity_id, object);
+ if (!ret)
+ ret = omci_reply_mib(info, OMCI_CMD_MIB_GET, odev, object,
+     0, omci_me_class_name(class_id));
+out:
+ mutex_unlock(&omci_devices_lock);
+ kfree(object);
+ return ret;
+}
+
+static int omci_cmd_mib_set(struct sk_buff *skb, struct genl_info *info)
+{
+ struct nlattr *data = info->attrs[OMCI_ATTR_ATTR_DATA];
+ struct omci_mib_object *object;
+ struct omci_device *odev;
+ int ret;
+
+ if (!info->attrs[OMCI_ATTR_CLASS_ID] ||
+    !info->attrs[OMCI_ATTR_ENTITY_ID])
+ return -EINVAL;
+
+ object = kzalloc(sizeof(*object), GFP_KERNEL);
+ if (!object)
+ return -ENOMEM;
+ object->origin = OMCI_MIB_ORIGIN_LOCAL;
+ object->class_id = nla_get_u16(info->attrs[OMCI_ATTR_CLASS_ID]);
+ object->entity_id = nla_get_u16(info->attrs[OMCI_ATTR_ENTITY_ID]);
+ object->attr_mask = info->attrs[OMCI_ATTR_ATTR_MASK] ?
+ nla_get_u16(info->attrs[OMCI_ATTR_ATTR_MASK]) : 0xffff;
+ if (data) {
+ object->data_len = min_t(size_t, nla_len(data),
+ sizeof(object->data));
+ memcpy(object->data, nla_data(data), object->data_len);
+ }
+
+ mutex_lock(&omci_devices_lock);
+ odev = omci_get_from_info(info);
+ if (!odev) {
+ ret = -ENODEV;
+ goto out;
+ }
+ ret = omci_agent_mib_set(odev, object);
+ if (!ret)
+ omci_device_notify(odev, OMCI_EVENT_MIB_CHANGE);
+out:
+ mutex_unlock(&omci_devices_lock);
+ kfree(object);
+ return ret;
+}
+
+static int omci_cmd_mib_delete(struct sk_buff *skb, struct genl_info *info)
+{
+ struct omci_device *odev;
+ u16 class_id;
+ u16 entity_id;
+ int ret;
+
+ if (!info->attrs[OMCI_ATTR_CLASS_ID] ||
+    !info->attrs[OMCI_ATTR_ENTITY_ID])
+ return -EINVAL;
+ class_id = nla_get_u16(info->attrs[OMCI_ATTR_CLASS_ID]);
+ entity_id = nla_get_u16(info->attrs[OMCI_ATTR_ENTITY_ID]);
+
+ mutex_lock(&omci_devices_lock);
+ odev = omci_get_from_info(info);
+ if (!odev) {
+ ret = -ENODEV;
+ goto out;
+ }
+ ret = omci_agent_mib_delete(odev, class_id, entity_id);
+ if (!ret)
+ omci_device_notify(odev, OMCI_EVENT_MIB_CHANGE);
+out:
+ mutex_unlock(&omci_devices_lock);
+ return ret;
+}
+
+static int omci_cmd_mib_reset(struct sk_buff *skb, struct genl_info *info)
+{
+ struct omci_device *odev;
+ int ret = 0;
+
+ mutex_lock(&omci_devices_lock);
+ odev = omci_get_from_info(info);
+ if (!odev) {
+ ret = -ENODEV;
+ goto out;
+ }
+ omci_agent_mib_reset(odev, true);
+ omci_device_notify(odev, OMCI_EVENT_MIB_CHANGE);
+out:
+ mutex_unlock(&omci_devices_lock);
+ return ret;
+}
+
+static int omci_cmd_mib_next(struct sk_buff *skb, struct genl_info *info)
+{
+ struct omci_mib_object *object;
+ struct omci_device *odev;
+ const char *name = NULL;
+ u32 index = 0;
+ u32 next_index;
+ int ret;
+
+ if (info->attrs[OMCI_ATTR_INDEX])
+ index = nla_get_u32(info->attrs[OMCI_ATTR_INDEX]);
+
+ object = kzalloc(sizeof(*object), GFP_KERNEL);
+ if (!object)
+ return -ENOMEM;
+
+ mutex_lock(&omci_devices_lock);
+ odev = omci_get_from_info(info);
+ if (!odev) {
+ ret = -ENODEV;
+ goto out;
+ }
+ ret = omci_agent_mib_next(odev, index, object, &next_index, &name);
+ if (!ret)
+ ret = omci_reply_mib(info, OMCI_CMD_MIB_NEXT, odev, object,
+     next_index, name);
+out:
+ mutex_unlock(&omci_devices_lock);
+ kfree(object);
+ return ret;
+}
+
+static const struct genl_ops omci_genl_ops[] = {
+ {
+ .cmd = OMCI_CMD_GET,
+ .policy = omci_policy,
+ .doit = omci_cmd_get,
+ },
+ {
+ .cmd = OMCI_CMD_BIND,
+ .flags = GENL_ADMIN_PERM,
+ .policy = omci_policy,
+ .doit = omci_cmd_bind,
+ },
+ {
+ .cmd = OMCI_CMD_UNBIND,
+ .flags = GENL_ADMIN_PERM,
+ .policy = omci_policy,
+ .doit = omci_cmd_unbind,
+ },
+ {
+ .cmd = OMCI_CMD_TX,
+ .flags = GENL_ADMIN_PERM,
+ .policy = omci_policy,
+ .doit = omci_cmd_tx,
+ },
+ {
+ .cmd = OMCI_CMD_AGENT_SET,
+ .flags = GENL_ADMIN_PERM,
+ .policy = omci_policy,
+ .doit = omci_cmd_agent_set,
+ },
+ {
+ .cmd = OMCI_CMD_CONFIG_GET,
+ .flags = GENL_ADMIN_PERM,
+ .policy = omci_policy,
+ .doit = omci_cmd_config_get,
+ },
+ {
+ .cmd = OMCI_CMD_CONFIG_SET,
+ .flags = GENL_ADMIN_PERM,
+ .policy = omci_policy,
+ .doit = omci_cmd_config_set,
+ },
+ {
+ .cmd = OMCI_CMD_MIB_GET,
+ .policy = omci_policy,
+ .doit = omci_cmd_mib_get,
+ },
+ {
+ .cmd = OMCI_CMD_MIB_SET,
+ .flags = GENL_ADMIN_PERM,
+ .policy = omci_policy,
+ .doit = omci_cmd_mib_set,
+ },
+ {
+ .cmd = OMCI_CMD_MIB_DELETE,
+ .flags = GENL_ADMIN_PERM,
+ .policy = omci_policy,
+ .doit = omci_cmd_mib_delete,
+ },
+ {
+ .cmd = OMCI_CMD_MIB_RESET,
+ .flags = GENL_ADMIN_PERM,
+ .policy = omci_policy,
+ .doit = omci_cmd_mib_reset,
+ },
+ {
+ .cmd = OMCI_CMD_MIB_NEXT,
+ .policy = omci_policy,
+ .doit = omci_cmd_mib_next,
+ },
+ {
+ .cmd = OMCI_CMD_CLASS_GET,
+ .policy = omci_policy,
+ .doit = omci_cmd_class_get,
+ },
+ {
+ .cmd = OMCI_CMD_CLASS_NEXT,
+ .policy = omci_policy,
+ .doit = omci_cmd_class_next,
+ },
+};
+
+static struct genl_family omci_genl_family __ro_after_init = {
+ .name = OMCI_GENL_NAME,
+ .version = OMCI_GENL_VERSION,
+ .maxattr = OMCI_ATTR_MAX,
+ .module = THIS_MODULE,
+ .ops = omci_genl_ops,
+ .n_ops = ARRAY_SIZE(omci_genl_ops),
+};
+
+static int omci_send(struct omci_device *odev, struct sk_buff *msg,
+     u32 portid, struct net *net)
+{
+ int ret;
+
+ ret = genlmsg_unicast(net, msg, portid);
+ if (ret == -ESRCH || ret == -ECONNREFUSED) {
+ struct net *owner_net = NULL;
+
+ mutex_lock(&odev->owner_lock);
+ if (odev->owner_portid == portid && odev->owner_net == net) {
+ owner_net = odev->owner_net;
+ odev->owner_net = NULL;
+ odev->owner_portid = 0;
+ }
+ mutex_unlock(&odev->owner_lock);
+ if (owner_net)
+ put_net(owner_net);
+ }
+
+ return ret;
+}
+
+static void omci_rx_work(struct work_struct *work)
+{
+ struct omci_device *odev;
+ struct sk_buff *skb;
+
+ odev = container_of(work, struct omci_device, rx_work);
+ while ((skb = skb_dequeue(&odev->rx_queue))) {
+ struct omci_skb_cb *cb = OMCI_SKB_CB(skb);
+ struct net *net = NULL;
+ struct sk_buff *msg;
+ u32 portid = 0;
+ u16 onu_id;
+ void *hdr;
+ int ret;
+
+ spin_lock_bh(&odev->state_lock);
+ onu_id = odev->onu_id;
+ if (cb->generation != odev->generation) {
+ spin_unlock_bh(&odev->state_lock);
+ atomic64_inc(&odev->rx_dropped);
+ dev_kfree_skb_any(skb);
+ continue;
+ }
+ spin_unlock_bh(&odev->state_lock);
+
+ omci_agent_receive(odev, skb);
+
+ mutex_lock(&odev->owner_lock);
+ if (odev->owner_portid && odev->owner_net) {
+ portid = odev->owner_portid;
+ net = get_net(odev->owner_net);
+ }
+ mutex_unlock(&odev->owner_lock);
+
+ if (!net) {
+ dev_kfree_skb_any(skb);
+ continue;
+ }
+
+ msg = genlmsg_new(NLMSG_DEFAULT_SIZE + skb->len, GFP_KERNEL);
+ if (!msg) {
+ atomic64_inc(&odev->rx_dropped);
+ put_net(net);
+ dev_kfree_skb_any(skb);
+ continue;
+ }
+
+ hdr = genlmsg_put(msg, 0, 0, &omci_genl_family, 0,
+  OMCI_CMD_RX);
+ if (!hdr ||
+    nla_put_u32(msg, OMCI_ATTR_DEV_ID, odev->id) ||
+    nla_put_u16(msg, OMCI_ATTR_ONU_ID, onu_id) ||
+    nla_put_u16(msg, OMCI_ATTR_GEM_PORT_ID, cb->gem_port_id) ||
+    nla_put_u32(msg, OMCI_ATTR_FLAGS, cb->flags) ||
+    nla_put_u64_64bit(msg, OMCI_ATTR_SEQUENCE, cb->sequence,
+      OMCI_ATTR_PAD) ||
+    nla_put(msg, OMCI_ATTR_PDU, skb->len, skb->data)) {
+ nlmsg_free(msg);
+ atomic64_inc(&odev->rx_dropped);
+ put_net(net);
+ dev_kfree_skb_any(skb);
+ continue;
+ }
+
+ genlmsg_end(msg, hdr);
+ ret = omci_send(odev, msg, portid, net);
+ if (ret)
+ atomic64_inc(&odev->rx_dropped);
+ put_net(net);
+ dev_kfree_skb_any(skb);
+ }
+}
+
+void omci_device_notify(struct omci_device *odev, u8 event)
+{
+ struct net *net = NULL;
+ struct sk_buff *msg;
+ u32 portid = 0;
+ void *hdr;
+
+ mutex_lock(&odev->owner_lock);
+ if (odev->owner_portid && odev->owner_net) {
+ portid = odev->owner_portid;
+ net = get_net(odev->owner_net);
+ }
+ mutex_unlock(&odev->owner_lock);
+ if (!net)
+ return;
+
+ msg = genlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
+ if (!msg)
+ goto out_put_net;
+
+ hdr = genlmsg_put(msg, 0, 0, &omci_genl_family, 0,
+  OMCI_CMD_EVENT);
+ if (!hdr || nla_put_u8(msg, OMCI_ATTR_EVENT, event) ||
+    omci_put_status(msg, odev)) {
+ nlmsg_free(msg);
+ goto out_put_net;
+ }
+
+ genlmsg_end(msg, hdr);
+ omci_send(odev, msg, portid, net);
+
+out_put_net:
+ put_net(net);
+}
+
+static int omci_netlink_notify(struct notifier_block *nb, unsigned long state,
+       void *data)
+{
+ struct netlink_notify *notify = data;
+ struct omci_device *odev;
+
+ if (state != NETLINK_URELEASE || notify->protocol != NETLINK_GENERIC)
+ return NOTIFY_DONE;
+
+ mutex_lock(&omci_devices_lock);
+ list_for_each_entry(odev, &omci_devices, list) {
+ struct net *owner_net = NULL;
+
+ mutex_lock(&odev->owner_lock);
+ if (odev->owner_portid == notify->portid &&
+    odev->owner_net == notify->net) {
+ owner_net = odev->owner_net;
+ odev->owner_net = NULL;
+ odev->owner_portid = 0;
+ }
+ mutex_unlock(&odev->owner_lock);
+ if (owner_net)
+ put_net(owner_net);
+ }
+ mutex_unlock(&omci_devices_lock);
+
+ return NOTIFY_DONE;
+}
+
+static struct notifier_block omci_netlink_nb = {
+ .notifier_call = omci_netlink_notify,
+};
+
+struct omci_device *
+omci_device_register(struct xpon_device *xpon, u32 capabilities,
+     const struct omci_device_ops *ops, void *priv)
+{
+ struct omci_device *odev;
+
+ if (!xpon || !ops || !ops->xmit)
+ return ERR_PTR(-EINVAL);
+ if (xpon->omci)
+ return ERR_PTR(-EBUSY);
+
+ odev = kzalloc(sizeof(*odev), GFP_KERNEL);
+ if (!odev)
+ return ERR_PTR(-ENOMEM);
+
+ INIT_LIST_HEAD(&odev->list);
+ mutex_init(&odev->lifecycle_lock);
+ mutex_init(&odev->owner_lock);
+ spin_lock_init(&odev->state_lock);
+ skb_queue_head_init(&odev->rx_queue);
+ INIT_WORK(&odev->rx_work, omci_rx_work);
+ odev->xpon = xpon;
+ odev->parent = xpon->class_dev;
+ odev->ifindex = xpon->netdev->ifindex;
+ odev->capabilities = capabilities | OMCI_CAP_BASELINE_AGENT;
+ odev->ops = ops;
+ odev->priv = priv;
+ odev->onu_id = 0xffff;
+ odev->gem_port_id = 0xffff;
+ odev->id = atomic_inc_return(&omci_next_id) - 1;
+
+ {
+ int ret = omci_agent_init(odev);
+
+ if (ret) {
+ kfree(odev);
+ return ERR_PTR(ret);
+ }
+
+ xpon->omci = odev;
+ ret = omci_sysfs_register(odev);
+ if (ret) {
+ xpon->omci = NULL;
+ omci_agent_cleanup(odev);
+ kfree(odev);
+ return ERR_PTR(ret);
+ }
+ }
+
+ mutex_lock(&omci_devices_lock);
+ list_add_tail(&odev->list, &omci_devices);
+ mutex_unlock(&omci_devices_lock);
+
+ dev_info(odev->parent, "registered OMCI agent device %u\n", odev->id);
+ return odev;
+}
+EXPORT_SYMBOL_GPL(omci_device_register);
+
+int omci_device_start(struct omci_device *odev)
+{
+ int ret = 0;
+
+ if (!odev)
+ return -EINVAL;
+
+ mutex_lock(&odev->lifecycle_lock);
+ if (odev->started)
+ goto out;
+
+ /*
+ * Publish the lifecycle state before entering the provider so state and
+ * receive callbacks generated synchronously by start() are accepted.
+ */
+ WRITE_ONCE(odev->started, true);
+ if (odev->ops->start) {
+ ret = odev->ops->start(odev);
+ if (ret)
+ WRITE_ONCE(odev->started, false);
+ }
+out:
+ mutex_unlock(&odev->lifecycle_lock);
+ return ret;
+}
+EXPORT_SYMBOL_GPL(omci_device_start);
+
+void omci_device_stop(struct omci_device *odev)
+{
+ if (!odev)
+ return;
+
+ mutex_lock(&odev->lifecycle_lock);
+ if (!odev->started)
+ goto out;
+
+ /* Let the provider drain the active OMCC before rejecting new traffic. */
+ if (odev->ops->stop)
+ odev->ops->stop(odev);
+ WRITE_ONCE(odev->started, false);
+out:
+ mutex_unlock(&odev->lifecycle_lock);
+}
+EXPORT_SYMBOL_GPL(omci_device_stop);
+
+void omci_device_unregister(struct omci_device *odev)
+{
+ struct net *owner_net;
+
+ if (!odev)
+ return;
+
+ mutex_lock(&omci_devices_lock);
+ list_del_init(&odev->list);
+ mutex_unlock(&omci_devices_lock);
+ omci_device_stop(odev);
+
+ cancel_work_sync(&odev->rx_work);
+ skb_queue_purge(&odev->rx_queue);
+
+ mutex_lock(&odev->owner_lock);
+ owner_net = odev->owner_net;
+ odev->owner_net = NULL;
+ odev->owner_portid = 0;
+ mutex_unlock(&odev->owner_lock);
+ if (owner_net)
+ put_net(owner_net);
+ omci_sysfs_unregister(odev);
+ if (odev->xpon && odev->xpon->omci == odev)
+ odev->xpon->omci = NULL;
+ omci_agent_cleanup(odev);
+ kfree(odev);
+}
+EXPORT_SYMBOL_GPL(omci_device_unregister);
+
+void *omci_device_priv(const struct omci_device *odev)
+{
+ return odev->priv;
+}
+EXPORT_SYMBOL_GPL(omci_device_priv);
+
+u32 omci_device_id(const struct omci_device *odev)
+{
+ return odev->id;
+}
+EXPORT_SYMBOL_GPL(omci_device_id);
+
+void omci_device_set_identity_info(struct omci_device *odev,
+   const struct omci_identity *identity)
+{
+ if (!odev || !identity)
+ return;
+ if (identity->valid & OMCI_IDENTITY_F_SERIAL_NUMBER)
+ omci_agent_config_set_source(odev, OMCI_CONFIG_SERIAL_NUMBER,
+     identity->serial_number,
+     sizeof(identity->serial_number),
+     identity->serial_source);
+ if (identity->valid & OMCI_IDENTITY_F_VENDOR_ID)
+ omci_agent_config_set_source(odev, OMCI_CONFIG_VENDOR_ID,
+     identity->vendor_id,
+     sizeof(identity->vendor_id),
+     identity->vendor_source);
+ if (identity->valid & OMCI_IDENTITY_F_PASSWORD)
+ omci_agent_config_set_source(odev, OMCI_CONFIG_PASSWORD,
+     identity->password,
+     sizeof(identity->password),
+     identity->password_source);
+ if (identity->valid & OMCI_IDENTITY_F_VERSION)
+ omci_agent_config_set_source(odev, OMCI_CONFIG_VERSION,
+     identity->version,
+     sizeof(identity->version),
+     identity->version_source);
+ if (identity->valid & OMCI_IDENTITY_F_EQUIPMENT_ID)
+ omci_agent_config_set_source(odev, OMCI_CONFIG_EQUIPMENT_ID,
+     identity->equipment_id,
+     sizeof(identity->equipment_id),
+     identity->equipment_source);
+}
+EXPORT_SYMBOL_GPL(omci_device_set_identity_info);
+
+void omci_device_set_identity(struct omci_device *odev,
+      const u8 serial_number[8],
+      const u8 password[10])
+{
+ struct omci_identity identity = {};
+
+ if (serial_number) {
+ memcpy(identity.serial_number, serial_number,
+       sizeof(identity.serial_number));
+ memcpy(identity.vendor_id, serial_number,
+       sizeof(identity.vendor_id));
+ identity.valid |= OMCI_IDENTITY_F_SERIAL_NUMBER |
+  OMCI_IDENTITY_F_VENDOR_ID;
+ identity.serial_source = OMCI_CONFIG_SOURCE_DRIVER;
+ identity.vendor_source = OMCI_CONFIG_SOURCE_DRIVER;
+ }
+ if (password) {
+ memcpy(identity.password, password, sizeof(identity.password));
+ identity.valid |= OMCI_IDENTITY_F_PASSWORD;
+ identity.password_source = OMCI_CONFIG_SOURCE_DRIVER;
+ }
+ omci_device_set_identity_info(odev, &identity);
+}
+EXPORT_SYMBOL_GPL(omci_device_set_identity);
+
+int omci_device_set_dying_gasp_enabled(struct omci_device *odev,
+       bool enabled, u8 source)
+{
+ u8 value = enabled;
+
+ if (!odev || source > OMCI_CONFIG_SOURCE_SYSFS)
+ return -EINVAL;
+
+ return omci_agent_config_set_source(odev,
+    OMCI_CONFIG_AGENT_DYING_GASP,
+    &value, sizeof(value), source);
+}
+EXPORT_SYMBOL_GPL(omci_device_set_dying_gasp_enabled);
+
+int omci_device_send_dying_gasp(struct omci_device *odev)
+{
+ if (!odev)
+ return -EINVAL;
+
+ return omci_agent_send_dying_gasp(odev);
+}
+EXPORT_SYMBOL_GPL(omci_device_send_dying_gasp);
+
+void omci_device_set_onu_id(struct omci_device *odev, u16 onu_id)
+{
+ spin_lock_bh(&odev->state_lock);
+ odev->onu_id = onu_id;
+ spin_unlock_bh(&odev->state_lock);
+}
+EXPORT_SYMBOL_GPL(omci_device_set_onu_id);
+
+void omci_device_set_channel(struct omci_device *odev, u16 gem_port_id,
+     bool valid)
+{
+ u8 event = valid ? OMCI_EVENT_CHANNEL_UP : OMCI_EVENT_CHANNEL_DOWN;
+ bool changed;
+
+ spin_lock_bh(&odev->state_lock);
+ changed = odev->channel_up != valid ||
+  (valid && odev->gem_port_id != gem_port_id);
+ if (changed)
+ odev->generation++;
+ odev->channel_up = valid;
+ odev->gem_port_id = valid ? gem_port_id : 0xffff;
+ spin_unlock_bh(&odev->state_lock);
+
+ if (!valid)
+ skb_queue_purge(&odev->rx_queue);
+ if (changed) {
+ omci_agent_channel_changed(odev, valid);
+ omci_device_notify(odev, event);
+ }
+}
+EXPORT_SYMBOL_GPL(omci_device_set_channel);
+
+void omci_device_set_state(struct omci_device *odev, u8 state)
+{
+ bool changed;
+
+ spin_lock_bh(&odev->state_lock);
+ changed = odev->state != state;
+ odev->state = state;
+ spin_unlock_bh(&odev->state_lock);
+
+ if (changed)
+ omci_device_notify(odev, OMCI_EVENT_STATE_CHANGE);
+}
+EXPORT_SYMBOL_GPL(omci_device_set_state);
+
+void omci_device_reset_session(struct omci_device *odev)
+{
+ if (!odev)
+ return;
+
+ /*
+ * Finish requests already accepted on the old OMCC before changing
+ * the generation. A Deactivate_ONU-ID PLOAM can race with an OMCI
+ * request carried by the same downstream frame.
+ */
+ if (current_work() != &odev->rx_work)
+ flush_work(&odev->rx_work);
+
+ spin_lock_bh(&odev->state_lock);
+ odev->generation++;
+ odev->onu_id = 0xffff;
+ odev->gem_port_id = 0xffff;
+ odev->channel_up = false;
+ spin_unlock_bh(&odev->state_lock);
+
+ if (current_work() != &odev->rx_work)
+ cancel_work_sync(&odev->rx_work);
+ skb_queue_purge(&odev->rx_queue);
+ omci_agent_channel_changed(odev, false);
+ omci_device_notify(odev, OMCI_EVENT_CHANNEL_DOWN);
+}
+EXPORT_SYMBOL_GPL(omci_device_reset_session);
+
+void omci_device_receive(struct omci_device *odev, struct sk_buff *skb,
+ u16 gem_port_id, u32 flags)
+{
+ struct omci_skb_cb *cb;
+ bool channel_up;
+ u16 configured_gem;
+ u32 generation;
+
+ if (!skb)
+ return;
+ if (!odev) {
+ dev_kfree_skb_any(skb);
+ return;
+ }
+ if (!READ_ONCE(odev->started)) {
+ atomic64_inc(&odev->rx_dropped);
+ dev_kfree_skb_any(skb);
+ return;
+ }
+
+ spin_lock_bh(&odev->state_lock);
+ channel_up = odev->channel_up;
+ configured_gem = odev->gem_port_id;
+ generation = odev->generation;
+ spin_unlock_bh(&odev->state_lock);
+
+ if (!channel_up || gem_port_id != configured_gem || !skb->len ||
+    skb->len > OMCI_MAX_PDU_LEN) {
+ atomic64_inc(&odev->rx_dropped);
+ dev_kfree_skb_any(skb);
+ return;
+ }
+
+ if (skb_queue_len(&odev->rx_queue) >= OMCI_RX_QUEUE_LEN) {
+ atomic64_inc(&odev->rx_dropped);
+ dev_kfree_skb_any(skb);
+ return;
+ }
+
+ cb = OMCI_SKB_CB(skb);
+ memset(cb, 0, sizeof(*cb));
+ cb->sequence = atomic64_inc_return(&odev->sequence);
+ cb->flags = flags;
+ cb->generation = generation;
+ cb->gem_port_id = gem_port_id;
+ atomic64_inc(&odev->rx_packets);
+ atomic64_add(skb->len, &odev->rx_bytes);
+ skb_queue_tail(&odev->rx_queue, skb);
+ schedule_work(&odev->rx_work);
+}
+EXPORT_SYMBOL_GPL(omci_device_receive);
+
+static int __init omci_init(void)
+{
+ int ret;
+
+ ret = omci_sysfs_init();
+ if (ret)
+ return ret;
+
+ ret = genl_register_family(&omci_genl_family);
+ if (ret)
+ goto err_sysfs;
+
+ ret = netlink_register_notifier(&omci_netlink_nb);
+ if (ret)
+ goto err_genl;
+
+ return 0;
+
+err_genl:
+ genl_unregister_family(&omci_genl_family);
+err_sysfs:
+ omci_sysfs_exit();
+ return ret;
+}
+module_init(omci_init);
+
+static void __exit omci_exit(void)
+{
+ netlink_unregister_notifier(&omci_netlink_nb);
+ genl_unregister_family(&omci_genl_family);
+ omci_sysfs_exit();
+}
+module_exit(omci_exit);
+
+MODULE_DESCRIPTION("Generic in-kernel OMCI agent");
+MODULE_LICENSE("GPL");
diff --git a/net/xpon/omci/identity.c b/net/xpon/omci/identity.c
new file mode 100644
index 000000000000..d71cc8caf078
--- /dev/null
+++ b/net/xpon/omci/identity.c
@@ -0,0 +1,466 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * OMCI identity loading and normalization
+ */
+
+#include <linux/ctype.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/export.h>
+#include <linux/kernel.h>
+#include <linux/nvmem-consumer.h>
+#include <linux/property.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+#include <linux/unaligned.h>
+
+#include "internal.h"
+
+#define OMCI_IDENTITY_MAX_INPUT_LEN 64
+
+static const u8 default_vendor_id[4] = { 'G', 'P', 'O', 'N' };
+
+static bool omci_source_can_replace(u8 current_source, u8 source)
+{
+ return source >= current_source;
+}
+
+static size_t omci_trim_input(const u8 **data, size_t len)
+{
+ while (len && isspace(**data)) {
+ (*data)++;
+ len--;
+ }
+ while (len && (data[0][len - 1] == '\0' ||
+       data[0][len - 1] == 0xff ||
+       isspace(data[0][len - 1])))
+ len--;
+
+ return len;
+}
+
+static int omci_hex_compact(const u8 *data, size_t len, u8 *output,
+    size_t output_len)
+{
+ size_t digits = 0;
+ size_t i;
+ int high = -1;
+
+ if (len >= 4 && !strncasecmp((const char *)data, "hex:", 4)) {
+ data += 4;
+ len -= 4;
+ } else if (len >= 2 && data[0] == '0' &&
+   (data[1] == 'x' || data[1] == 'X')) {
+ data += 2;
+ len -= 2;
+ }
+
+ memset(output, 0, output_len);
+ for (i = 0; i < len; i++) {
+ int value;
+
+ if (data[i] == ':' || data[i] == '-' || data[i] == '.' ||
+    data[i] == '_' || isspace(data[i]))
+ continue;
+ value = hex_to_bin(data[i]);
+ if (value < 0)
+ return -EINVAL;
+ if (high < 0) {
+ high = value;
+ } else {
+ if (digits / 2 >= output_len)
+ return -E2BIG;
+ output[digits / 2] = (high << 4) | value;
+ high = -1;
+ }
+ digits++;
+ }
+
+ if (high >= 0 || digits != output_len * 2)
+ return -EINVAL;
+
+ return 0;
+}
+
+static bool omci_all_hex(const u8 *data, size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < len; i++)
+ if (hex_to_bin(data[i]) < 0)
+ return false;
+ return true;
+}
+
+static int omci_normalize_serial(const u8 *data, size_t len, u8 serial[8])
+{
+ const u8 *trimmed = data;
+ u8 suffix[4];
+ int ret;
+
+
+ switch (len) {
+ case 8:
+ memcpy(serial, data, 8);
+ return 0;
+ case 16:
+ if (omci_all_hex(trimmed, 16))
+ return omci_hex_compact(trimmed, 16, serial, 8);
+ break;
+ }
+ len = omci_trim_input(&trimmed, len);
+ if (len == 12 && omci_all_hex(trimmed + 4, 8)) {
+ ret = omci_hex_compact(trimmed + 4, 8, suffix, sizeof(suffix));
+ if (ret)
+ return ret;
+ memcpy(serial, trimmed, 4);
+ memcpy(serial + 4, suffix, sizeof(suffix));
+ return 0;
+ }
+
+ return omci_hex_compact(trimmed, len, serial, 8);
+}
+
+static int omci_normalize_vendor(const u8 *data, size_t len, u8 vendor[4])
+{
+ const u8 *trimmed = data;
+
+ if (len == 4) {
+ memcpy(vendor, data, 4);
+ return 0;
+ }
+ len = omci_trim_input(&trimmed, len);
+ return omci_hex_compact(trimmed, len, vendor, 4);
+}
+
+static int omci_normalize_password(const u8 *data, size_t len,
+   u8 password[10])
+{
+ const u8 *trimmed = data;
+
+ if (len == 10) {
+ memcpy(password, data, 10);
+ return 0;
+ }
+ len = omci_trim_input(&trimmed, len);
+ if (!len)
+ return -EINVAL;
+ if (len == 20 || (len > 4 && !strncasecmp((const char *)trimmed, "hex:", 4)))
+ return omci_hex_compact(trimmed, len, password, 10);
+ if (len > 10)
+ return -E2BIG;
+
+ memset(password, 0, 10);
+ memcpy(password, trimmed, len);
+ return 0;
+}
+
+static int omci_normalize_string(const u8 *data, size_t len, u8 *output,
+ size_t output_len)
+{
+ const u8 *trimmed = data;
+
+ len = omci_trim_input(&trimmed, len);
+ if (!len || len > output_len)
+ return -EINVAL;
+ memset(output, 0, output_len);
+ memcpy(output, trimmed, len);
+ return 0;
+}
+
+static int omci_identity_set_serial(struct omci_identity *identity,
+    const u8 *data, size_t len, u8 source)
+{
+ u8 serial[8];
+ int ret;
+
+ ret = omci_normalize_serial(data, len, serial);
+ if (ret)
+ return ret;
+ if (omci_source_can_replace(identity->serial_source, source)) {
+ memcpy(identity->serial_number, serial, sizeof(serial));
+ identity->serial_source = source;
+ identity->valid |= OMCI_IDENTITY_F_SERIAL_NUMBER;
+ }
+ if (omci_source_can_replace(identity->vendor_source, source)) {
+ memcpy(identity->vendor_id, serial, sizeof(identity->vendor_id));
+ identity->vendor_source = source;
+ identity->valid |= OMCI_IDENTITY_F_VENDOR_ID;
+ }
+ return 0;
+}
+
+static int omci_identity_set_vendor(struct omci_identity *identity,
+    const u8 *data, size_t len, u8 source)
+{
+ u8 vendor[4];
+ int ret;
+
+ ret = omci_normalize_vendor(data, len, vendor);
+ if (ret)
+ return ret;
+ if (!omci_source_can_replace(identity->vendor_source, source))
+ return 0;
+
+ memcpy(identity->vendor_id, vendor, sizeof(vendor));
+ identity->vendor_source = source;
+ identity->valid |= OMCI_IDENTITY_F_VENDOR_ID;
+ if (identity->valid & OMCI_IDENTITY_F_SERIAL_NUMBER)
+ memcpy(identity->serial_number, vendor, sizeof(vendor));
+ return 0;
+}
+
+static int omci_identity_set_password(struct omci_identity *identity,
+      const u8 *data, size_t len, u8 source)
+{
+ u8 password[10];
+ int ret;
+
+ ret = omci_normalize_password(data, len, password);
+ if (ret)
+ return ret;
+ if (!omci_source_can_replace(identity->password_source, source))
+ return 0;
+
+ memcpy(identity->password, password, sizeof(password));
+ identity->password_source = source;
+ identity->valid |= OMCI_IDENTITY_F_PASSWORD;
+ return 0;
+}
+
+static int __maybe_unused omci_identity_set_version(struct
omci_identity *identity,
+    const u8 *data,
+    size_t len, u8 source)
+{
+ u8 version[OMCI_OLT_VERSION_LEN];
+ int ret;
+
+ ret = omci_normalize_string(data, len, version, sizeof(version));
+ if (ret)
+ return ret;
+ if (!omci_source_can_replace(identity->version_source, source))
+ return 0;
+
+ memcpy(identity->version, version, sizeof(version));
+ identity->version_source = source;
+ identity->valid |= OMCI_IDENTITY_F_VERSION;
+ return 0;
+}
+
+static int __maybe_unused omci_identity_set_equipment(struct
omci_identity *identity,
+      const u8 *data,
+      size_t len, u8 source)
+{
+ u8 equipment[OMCI_OLT_EQUIPMENT_ID_LEN];
+ int ret;
+
+ ret = omci_normalize_string(data, len, equipment, sizeof(equipment));
+ if (ret)
+ return ret;
+ if (!omci_source_can_replace(identity->equipment_source, source))
+ return 0;
+
+ memcpy(identity->equipment_id, equipment, sizeof(equipment));
+ identity->equipment_source = source;
+ identity->valid |= OMCI_IDENTITY_F_EQUIPMENT_ID;
+ return 0;
+}
+
+int omci_identity_normalize_config(u16 key, const void *value, size_t len,
+   u8 *output, size_t *output_len)
+{
+ size_t required;
+ int ret;
+
+ if (!value || !output || !output_len)
+ return -EINVAL;
+ switch (key) {
+ case OMCI_CONFIG_SERIAL_NUMBER:
+ required = 8;
+ if (*output_len < required)
+ return -ENOSPC;
+ ret = omci_normalize_serial(value, len, output);
+ break;
+ case OMCI_CONFIG_VENDOR_ID:
+ required = 4;
+ if (*output_len < required)
+ return -ENOSPC;
+ ret = omci_normalize_vendor(value, len, output);
+ break;
+ case OMCI_CONFIG_PASSWORD:
+ required = 10;
+ if (*output_len < required)
+ return -ENOSPC;
+ ret = omci_normalize_password(value, len, output);
+ break;
+ case OMCI_CONFIG_VERSION:
+ case OMCI_CONFIG_HARDWARE_VERSION:
+ case OMCI_CONFIG_SOFTWARE_VERSION_0:
+ case OMCI_CONFIG_SOFTWARE_VERSION_1:
+ required = OMCI_HARDWARE_VERSION_LEN;
+ if (*output_len < required)
+ return -ENOSPC;
+ ret = omci_normalize_string(value, len, output, required);
+ break;
+ case OMCI_CONFIG_EQUIPMENT_ID:
+ required = OMCI_OLT_EQUIPMENT_ID_LEN;
+ if (*output_len < required)
+ return -ENOSPC;
+ ret = omci_normalize_string(value, len, output, required);
+ break;
+ default:
+ return -EOPNOTSUPP;
+ }
+ if (ret)
+ return ret;
+ *output_len = required;
+ return 0;
+}
+
+typedef int (*omci_identity_setter_t)(struct omci_identity *identity,
+      const u8 *data, size_t len, u8 source);
+
+static int omci_identity_read_property(struct device *dev,
+       struct omci_identity *identity,
+       const char *name,
+       omci_identity_setter_t setter)
+{
+ u8 data[OMCI_IDENTITY_MAX_INPUT_LEN];
+ const char *string;
+ int count;
+ int ret;
+
+ ret = device_property_read_string(dev, name, &string);
+ if (!ret)
+ return setter(identity, string, strlen(string),
+      OMCI_CONFIG_SOURCE_DEVICE_TREE);
+
+ count = device_property_count_u8(dev, name);
+ if (count < 0)
+ return count == -EINVAL || count == -ENODATA ? 0 : count;
+ if (!count || count > sizeof(data))
+ return -EINVAL;
+ ret = device_property_read_u8_array(dev, name, data, count);
+ if (ret)
+ return ret;
+ return setter(identity, data, count, OMCI_CONFIG_SOURCE_DEVICE_TREE);
+}
+
+static int omci_identity_read_cell(struct device *dev,
+   struct omci_identity *identity,
+   const char *name,
+   omci_identity_setter_t setter)
+{
+ struct nvmem_cell *cell;
+ void *data;
+ size_t len;
+ int ret;
+
+ cell = nvmem_cell_get(dev, name);
+ if (IS_ERR(cell)) {
+ ret = PTR_ERR(cell);
+ if (ret == -ENOENT || ret == -ENODEV || ret == -EOPNOTSUPP)
+ return 0;
+ return ret;
+ }
+ data = nvmem_cell_read(cell, &len);
+ nvmem_cell_put(cell);
+ if (IS_ERR(data))
+ return PTR_ERR(data);
+ ret = setter(identity, data, len, OMCI_CONFIG_SOURCE_NVMEM);
+ kfree(data);
+ return ret;
+}
+
+static int omci_identity_load_properties(struct device *dev,
+ struct omci_identity *identity)
+{
+ static const struct {
+ const char *name;
+ omci_identity_setter_t setter;
+ } properties[] = {
+ { "omci-vendor-id", omci_identity_set_vendor },
+ };
+ unsigned int i;
+ int ret;
+
+ for (i = 0; i < ARRAY_SIZE(properties); i++) {
+ ret = omci_identity_read_property(dev, identity,
+  properties[i].name,
+  properties[i].setter);
+ if (ret)
+ return dev_err_probe(dev, ret,
+     "invalid %s identity property\n",
+     properties[i].name);
+ }
+ return 0;
+}
+
+static int omci_identity_load_nvmem(struct device *dev,
+    struct omci_identity *identity)
+{
+ static const struct {
+ const char *name;
+ omci_identity_setter_t setter;
+ } cells[] = {
+ { "gpon-serial-number", omci_identity_set_serial },
+ { "gpon-password", omci_identity_set_password },
+ };
+ unsigned int i;
+ int ret;
+
+ for (i = 0; i < ARRAY_SIZE(cells); i++) {
+ ret = omci_identity_read_cell(dev, identity, cells[i].name,
+      cells[i].setter);
+ if (ret)
+ return dev_err_probe(dev, ret,
+     "failed to read %s NVMEM cell\n",
+     cells[i].name);
+ }
+ return 0;
+}
+
+int omci_identity_load(struct device *dev, struct omci_identity *identity)
+{
+ int ret;
+
+ if (!dev || !identity)
+ return -EINVAL;
+ memset(identity, 0, sizeof(*identity));
+
+ ret = omci_identity_load_properties(dev, identity);
+ if (ret)
+ return ret;
+
+ return omci_identity_load_nvmem(dev, identity);
+}
+EXPORT_SYMBOL_GPL(omci_identity_load);
+
+void gpon_random_serial_number(const u8 vendor[4], struct
omci_identity *identity)
+{
+ u32 serial;
+
+ /*
+ * GPON serial numbers contain a four-byte vendor identifier followed by
+ * a four-byte vendor-specific value. Keep a configured vendor identifier
+ * when available and randomize only the vendor-specific part.
+ */
+ if (!(identity->valid & OMCI_IDENTITY_F_VENDOR_ID)) {
+ memcpy(identity->vendor_id,
+       (vendor == (u8[4]){0,0,0,0}) ? default_vendor_id : vendor,
+       sizeof(identity->vendor_id));
+ identity->valid |= OMCI_IDENTITY_F_VENDOR_ID;
+ identity->vendor_source = OMCI_CONFIG_SOURCE_DEFAULT;
+ }
+
+ do {
+ serial = get_random_u32();
+ } while (!serial || serial == U32_MAX);
+
+ memcpy(identity->serial_number, identity->vendor_id,
+       sizeof(identity->vendor_id));
+ put_unaligned_be32(serial, identity->serial_number + 4);
+ identity->valid |= OMCI_IDENTITY_F_SERIAL_NUMBER;
+ identity->serial_source = OMCI_CONFIG_SOURCE_DRIVER;
+}
+EXPORT_SYMBOL_GPL(gpon_random_serial_number);
diff --git a/net/xpon/omci/internal.h b/net/xpon/omci/internal.h
new file mode 100644
index 000000000000..9370fe365850
--- /dev/null
+++ b/net/xpon/omci/internal.h
@@ -0,0 +1,203 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _NET_OMCI_INTERNAL_H
+#define _NET_OMCI_INTERNAL_H
+
+#include <linux/atomic.h>
+#include <linux/device.h>
+#include <linux/list.h>
+#include <linux/mutex.h>
+#include <linux/skbuff.h>
+#include <linux/spinlock.h>
+#include <linux/types.h>
+#include <linux/workqueue.h>
+#include <linux/xarray.h>
+#include <net/net_namespace.h>
+#include <net/xpon/omci.h>
+
+#include "me.h"
+
+#define OMCI_RX_QUEUE_LEN 256
+#define OMCI_BASELINE_DEV_ID 0x0a
+#define OMCI_EXTENDED_DEV_ID 0x0b
+#define OMCI_BASELINE_LEN 48
+#define OMCI_BASELINE_LEN_NO_MIC 44
+#define OMCI_EXTENDED_HEADER_LEN 10
+#define OMCI_MIC_LEN 4
+
+struct omci_skb_cb {
+ u64 sequence;
+ u32 flags;
+ u32 generation;
+ u16 gem_port_id;
+};
+
+#define OMCI_SKB_CB(_skb) ((struct omci_skb_cb *)&((_skb)->cb[0]))
+
+struct omci_mib_object {
+ u16 class_id;
+ u16 entity_id;
+ u16 attr_mask;
+ u8 origin;
+ u8 owner_profile;
+ u8 data_len;
+ bool pending_delete;
+ u8 data[OMCI_MAX_ATTR_DATA];
+ struct omci_olt_g olt_g;
+ struct omci_vlan_tagging_filter vlan_filter;
+ struct omci_extended_vlan extended_vlan;
+};
+
+struct omci_service_state {
+ struct omci_service_config config;
+};
+
+struct omci_agent_config {
+ u8 serial_number[8];
+ u8 vendor_id[4];
+ u8 version[OMCI_HARDWARE_VERSION_LEN];
+ u8 software_version[2][OMCI_SOFTWARE_VERSION_LEN];
+ u8 equipment_id[20];
+ u8 password[10];
+ u8 serial_source;
+ u8 vendor_source;
+ u8 version_source;
+ u8 software_version_source[2];
+ u8 omcc_version;
+ u8 omcc_version_source;
+ u8 equipment_source;
+ u8 password_source;
+ u8 traffic_mgmt_option;
+ u8 onu_type;
+ u8 onu_type_source;
+ u8 uni_count;
+ u8 olt_profile;
+ u8 olt_profile_force;
+ u8 olt_profile_source;
+ u8 olt_profile_force_source;
+ u8 dying_gasp_source;
+};
+
+struct omci_agent {
+ /* Serializes configuration, MIB and transaction state. */
+ struct mutex lock;
+ struct xarray mib;
+ struct xarray services;
+ struct omci_agent_config config;
+ u32 upload_index;
+ u16 mib_sync;
+ bool enabled;
+ bool permissive;
+ bool fake_omci;
+ bool dying_gasp;
+ bool operational;
+ u8 alarm_sequence;
+ u8 profile_effective;
+ u32 profile_quirks;
+
+ struct sk_buff *last_request;
+ struct sk_buff *last_response;
+ u32 last_request_hash;
+ bool last_response_fake;
+ u8 *table_snapshot;
+ size_t table_snapshot_len;
+ unsigned long table_snapshot_jiffies;
+ u16 table_snapshot_class_id;
+ u16 table_snapshot_entity_id;
+ u16 table_snapshot_mask;
+
+ atomic64_t responses;
+ atomic64_t duplicates;
+ atomic64_t unsupported;
+ atomic64_t fake_responses;
+};
+
+struct omci_device {
+ struct list_head list;
+ struct xpon_device *xpon;
+ struct device *parent;
+ const struct omci_device_ops *ops;
+ void *priv;
+ u32 id;
+ u32 ifindex;
+ u32 capabilities;
+ /* Serializes provider transport ownership. */
+ struct mutex lifecycle_lock;
+ bool started;
+
+ /* Protect the optional userspace observer. */
+ struct mutex owner_lock;
+ struct net *owner_net;
+ u32 owner_portid;
+
+ /* Protect channel state accessed from the hardware RX path. */
+ spinlock_t state_lock;
+ u16 onu_id;
+ u16 gem_port_id;
+ u32 generation;
+ u8 state;
+ bool channel_up;
+
+ struct sk_buff_head rx_queue;
+ struct work_struct rx_work;
+ atomic64_t sequence;
+ atomic64_t rx_packets;
+ atomic64_t rx_bytes;
+ atomic64_t rx_dropped;
+ atomic64_t tx_packets;
+ atomic64_t tx_bytes;
+ atomic64_t tx_errors;
+
+ struct omci_agent agent;
+};
+
+int omci_validate_tx(struct omci_device *odev, struct sk_buff *skb);
+int omci_device_xmit(struct omci_device *odev, const void *data, size_t len);
+void omci_device_notify(struct omci_device *odev, u8 event);
+
+int omci_agent_init(struct omci_device *odev);
+void omci_agent_cleanup(struct omci_device *odev);
+void omci_agent_receive(struct omci_device *odev, const struct sk_buff *skb);
+void omci_agent_channel_changed(struct omci_device *odev, bool valid);
+int omci_agent_send_dying_gasp(struct omci_device *odev);
+int omci_agent_put_status(struct sk_buff *msg, struct omci_device *odev);
+int omci_agent_config_get(struct omci_device *odev, u16 key,
+  void *value, size_t *len);
+int omci_agent_config_set(struct omci_device *odev, u16 key,
+  const void *value, size_t len);
+int omci_agent_config_set_userspace(struct omci_device *odev, u16 key,
+    const void *value, size_t len, u8 source);
+int omci_agent_config_set_source(struct omci_device *odev, u16 key,
+ const void *value, size_t len, u8 source);
+int omci_agent_config_source_get(struct omci_device *odev, u16 key,
u8 *source);
+int omci_identity_normalize_config(u16 key, const void *value, size_t len,
+   u8 *output, size_t *output_len);
+int omci_agent_mib_get(struct omci_device *odev, u16 class_id, u16 entity_id,
+       struct omci_mib_object *object);
+int omci_agent_mib_set(struct omci_device *odev,
+       const struct omci_mib_object *object);
+int omci_agent_mib_delete(struct omci_device *odev, u16 class_id,
+  u16 entity_id);
+void omci_agent_mib_reset(struct omci_device *odev, bool all);
+int omci_agent_mib_next(struct omci_device *odev, u32 index,
+ struct omci_mib_object *object, u32 *next_index,
+ const char **name);
+int omci_agent_olt_g_get(struct omci_device *odev, struct omci_olt_g *olt);
+
+bool omci_profile_valid(u8 profile);
+bool omci_profile_forceable(u8 profile);
+u8 omci_profile_detect(const struct omci_olt_g *olt);
+u32 omci_profile_quirks(u8 profile);
+void omci_profile_sanitize_olt_g(struct omci_olt_g *olt, u32 quirks);
+u16 omci_profile_normalize_uni_entity(u8 profile, u16 entity_id);
+void omci_profile_normalize_vlan_rule(u8 profile,
+      struct omci_extended_vlan_rule *rule);
+int omci_profile_resolve_multicast_ani(u8 profile,
+       u16 bridge_port_entity_id,
+       u16 *ani_entity_id);
+
+int omci_sysfs_init(void);
+void omci_sysfs_exit(void);
+int omci_sysfs_register(struct omci_device *odev);
+void omci_sysfs_unregister(struct omci_device *odev);
+
+#endif /* _NET_OMCI_INTERNAL_H */
diff --git a/net/xpon/omci/me.c b/net/xpon/omci/me.c
new file mode 100644
index 000000000000..35c307e52daf
--- /dev/null
+++ b/net/xpon/omci/me.c
@@ -0,0 +1,1210 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * OMCI managed entity catalogue and attribute descriptors
+ */
+
+#include <linux/errno.h>
+#include <linux/kernel.h>
+#include <linux/string.h>
+#include <net/xpon/omci.h>
+
+#include "internal.h"
+#include "me.h"
+
+struct omci_class_name {
+ u16 class_id;
+ const char *name;
+};
+
+static const struct omci_class_name omci_classes[] = {
+ { 1, "ONT" },
+ { 2, "ONU data" },
+ { 3, "PON IF line cardholder" },
+ { 4, "PON IF line card" },
+ { 5, "Cardholder" },
+ { 6, "Circuit pack" },
+ { 7, "Software image" },
+ { 8, "UNI" },
+ { 9, "TC adapter" },
+ { 10, "Physical path termination point ATM UNI" },
+ { 11, "Physical path termination point Ethernet UNI" },
+ { 12, "Physical path termination point CES UNI" },
+ { 13, "Logical N x 64 kbit/s sub-port connection termination point (CTP)" },
+ { 14, "Interworking VCC termination point" },
+ { 15, "AAL 1 profile" },
+ { 16, "AAL 5 profile" },
+ { 17, "AAL 1 protocol monitoring history data" },
+ { 18, "AAL 5 performance monitoring history data" },
+ { 19, "AAL 2 profile" },
+ { 20, "Intentionally left blank" },
+ { 21, "CES service profile" },
+ { 22, "Reserved" },
+ { 23, "CES physical interface performance monitoring history data" },
+ { 24, "Ethernet performance monitoring history data" },
+ { 25, "VP network CTP" },
+ { 26, "ATM VP cross-connection" },
+ { 27, "Priority queue" },
+ { 28, "DBR/CBR traffic descriptor" },
+ { 29, "UBR traffic descriptor" },
+ { 30, "SBR1/VBR1 traffic descriptor" },
+ { 31, "SBR2/VBR2 traffic descriptor" },
+ { 32, "SBR3/VBR3 traffic descriptor" },
+ { 33, "ABR traffic descriptor" },
+ { 34, "GFR traffic descriptor" },
+ { 35, "ABT/DT/IT traffic descriptor" },
+ { 36, "UPC disagreement monitoring history data" },
+ { 37, "Intentionally left blank" },
+ { 38, "ANI" },
+ { 39, "PON TC adapter" },
+ { 40, "PON physical path termination point" },
+ { 41, "TC adapter protocol monitoring history data" },
+ { 42, "Threshold data" },
+ { 43, "Operator specific" },
+ { 44, "Vendor specific" },
+ { 45, "MAC bridge service profile" },
+ { 46, "MAC bridge configuration data" },
+ { 47, "MAC bridge port configuration data" },
+ { 48, "MAC bridge port designation data" },
+ { 49, "MAC bridge port filter table data" },
+ { 50, "MAC bridge port bridge table data" },
+ { 51, "MAC bridge performance monitoring history data" },
+ { 52, "MAC bridge port performance monitoring history data" },
+ { 53, "Physical path termination point POTS UNI" },
+ { 54, "Voice CTP" },
+ { 55, "Voice PM history data" },
+ { 56, "AAL2 PVC profile" },
+ { 57, "AAL2 CPS protocol monitoring history data" },
+ { 58, "Voice service profile" },
+ { 59, "LES service profile" },
+ { 60, "AAL2 SSCS parameter profile 1" },
+ { 61, "AAL2 SSCS parameter profile 2" },
+ { 62, "VP performance monitoring history data" },
+ { 63, "Traffic scheduler" },
+ { 64, "T-CONT buffer" },
+ { 65, "UBR+ traffic descriptor" },
+ { 66, "AAL2 SSCS protocol monitoring history data" },
+ { 67, "IP port configuration data" },
+ { 68, "IP router service profile" },
+ { 69, "IP router configuration data" },
+ { 70, "IP router performance monitoring history data 1" },
+ { 71, "IP router performance monitoring history data 2" },
+ { 72, "ICMP performance monitoring history data 1" },
+ { 73, "ICMP performance monitoring history data 2" },
+ { 74, "IP route table" },
+ { 75, "IP static routes" },
+ { 76, "ARP service profile" },
+ { 77, "ARP configuration data" },
+ { 78, "VLAN tagging operation configuration data" },
+ { 79, "MAC bridge port filter pre-assign table" },
+ { 80, "Physical path termination point ISDN UNI" },
+ { 81, "Reserved" },
+ { 82, "Physical path termination point video UNI" },
+ { 83, "Physical path termination point LCT UNI" },
+ { 84, "VLAN tagging filter data" },
+ { 85, "ONU" },
+ { 86, "ATM VC cross-connection" },
+ { 87, "VC network CTP" },
+ { 88, "VC PM history data" },
+ { 89, "Ethernet performance monitoring history data 2" },
+ { 90, "Physical path termination point video ANI" },
+ { 91, "Physical path termination point IEEE 802.11 UNI" },
+ { 92, "IEEE 802.11 station management data 1" },
+ { 93, "IEEE 802.11 station management data 2" },
+ { 94, "IEEE 802.11 general purpose object" },
+ { 95, "IEEE 802.11 MAC and PHY operation and antenna data" },
+ { 96, "IEEE 802.11 performance monitoring history data" },
+ { 97, "IEEE 802.11 PHY FHSS DSSS IR tables" },
+ { 98, "Physical path termination point xDSL UNI part 1" },
+ { 99, "Physical path termination point xDSL UNI part 2" },
+ { 100, "xDSL line inventory and status data part 1" },
+ { 101, "xDSL line inventory and status data part 2" },
+ { 102, "xDSL channel downstream status data" },
+ { 103, "xDSL channel upstream status data" },
+ { 104, "xDSL line configuration profile part 1" },
+ { 105, "xDSL line configuration profile part 2" },
+ { 106, "xDSL line configuration profile part 3" },
+ { 107, "xDSL channel configuration profile" },
+ { 108, "xDSL subcarrier masking downstream profile" },
+ { 109, "xDSL subcarrier masking upstream profile" },
+ { 110, "xDSL PSD mask profile" },
+ { 111, "xDSL downstream radio frequency interference (RFI) bands profile" },
+ { 112, "xDSL xTU-C performance monitoring history data" },
+ { 113, "xDSL xTU-R performance monitoring history data" },
+ { 114, "xDSL xTU-C channel performance monitoring history data" },
+ { 115, "xDSL xTU-R channel performance monitoring history data" },
+ { 116, "TC adaptor performance monitoring history data xDSL" },
+ { 117, "Physical path termination point VDSL UNI (ITU-T G.993.1 VDSL1)" },
+ { 118, "VDSL VTU-O physical data" },
+ { 119, "VDSL VTU-R physical data" },
+ { 120, "VDSL channel data" },
+ { 121, "VDSL line configuration profile" },
+ { 122, "VDSL channel configuration profile" },
+ { 123, "VDSL band plan configuration profile" },
+ { 124, "VDSL VTU-O physical interface monitoring history data" },
+ { 125, "VDSL VTU-R physical interface monitoring history data" },
+ { 126, "VDSL VTU-O channel performance monitoring history data" },
+ { 127, "VDSL VTU-R channel performance monitoring history data" },
+ { 128, "Video return path service profile" },
+ { 129, "Video return path performance monitoring history data" },
+ { 130, "IEEE 802.1p mapper service profile" },
+ { 131, "OLT-G" },
+ { 132, "Multicast interworking VCC termination point" },
+ { 133, "ONU power shedding" },
+ { 134, "IP host config data" },
+ { 135, "IP host performance monitoring history data" },
+ { 136, "TCP/UDP config data" },
+ { 137, "Network address" },
+ { 138, "VoIP config data" },
+ { 139, "VoIP voice CTP" },
+ { 140, "Call control performance monitoring history data" },
+ { 141, "VoIP line status" },
+ { 142, "VoIP media profile" },
+ { 143, "RTP profile data" },
+ { 144, "RTP performance monitoring history data" },
+ { 145, "Network dial plan table" },
+ { 146, "VoIP application service profile" },
+ { 147, "VoIP feature access codes" },
+ { 148, "Authentication security method" },
+ { 149, "SIP config portal" },
+ { 150, "SIP agent config data" },
+ { 151, "SIP agent performance monitoring history data" },
+ { 152, "SIP call initiation performance monitoring history data" },
+ { 153, "SIP user data" },
+ { 154, "MGC config portal" },
+ { 155, "MGC config data" },
+ { 156, "MGC performance monitoring history data" },
+ { 157, "Large string" },
+ { 158, "ONU remote debug" },
+ { 159, "Equipment protection profile" },
+ { 160, "Equipment extension package" },
+ { 161, "Port-mapping package (legacy B-PON)" },
+ { 162, "Physical path termination point MoCA UNI" },
+ { 163, "MoCA Ethernet performance monitoring history data" },
+ { 164, "MoCA interface performance monitoring history data" },
+ { 165, "VDSL2 line configuration extensions" },
+ { 166, "xDSL line inventory and status data part 3" },
+ { 167, "xDSL line inventory and status data part 4" },
+ { 168, "VDSL2 line inventory and status data part 1" },
+ { 169, "VDSL2 line inventory and status data part 2" },
+ { 170, "VDSL2 line inventory and status data part 3" },
+ { 171, "Extended VLAN tagging operation configuration data" },
+ { 256, "ONU-G" },
+ { 257, "ONU2-G" },
+ { 258, "ONU-G (deprecated)" },
+ { 259, "ONU2-G (deprecated)" },
+ { 260, "PON IF line card-G" },
+ { 261, "PON TC adapter-G" },
+ { 262, "T-CONT" },
+ { 263, "ANI-G" },
+ { 264, "UNI-G" },
+ { 265, "ATM interworking VCC termination point" },
+ { 266, "GEM interworking termination point" },
+ { 267, "GEM port performance monitoring history data (obsolete)" },
+ { 268, "GEM port network CTP" },
+ { 269, "VP network CTP" },
+ { 270, "VC network CTP-G" },
+ { 271, "GAL TDM profile (deprecated)" },
+ { 272, "GAL Ethernet profile" },
+ { 273, "Threshold data 1" },
+ { 274, "Threshold data 2" },
+ { 275, "GAL TDM performance monitoring history data (deprecated)" },
+ { 276, "GAL Ethernet performance monitoring history data" },
+ { 277, "Priority queue" },
+ { 278, "Traffic scheduler" },
+ { 279, "Protection data" },
+ { 280, "Traffic descriptor" },
+ { 281, "Multicast GEM interworking termination point" },
+ { 282, "Pseudowire termination point" },
+ { 283, "RTP pseudowire parameters" },
+ { 284, "Pseudowire maintenance profile" },
+ { 285, "Pseudowire performance monitoring history data" },
+ { 286, "Ethernet flow termination point" },
+ { 287, "OMCI" },
+ { 288, "Managed entity" },
+ { 289, "Attribute" },
+ { 290, "Dot1X port extension package" },
+ { 291, "Dot1X configuration profile" },
+ { 292, "Dot1X performance monitoring history data" },
+ { 293, "Radius performance monitoring history data" },
+ { 294, "TU CTP" },
+ { 295, "TU performance monitoring history data" },
+ { 296, "Ethernet performance monitoring history data 3" },
+ { 297, "Port-mapping package" },
+ { 298, "Dot1 rate limiter" },
+ { 299, "Dot1ag maintenance domain" },
+ { 300, "Dot1ag maintenance association" },
+ { 301, "Dot1ag default MD level" },
+ { 302, "Dot1ag MEP" },
+ { 303, "Dot1ag MEP status" },
+ { 304, "Dot1ag MEP CCM database" },
+ { 305, "Dot1ag CFM stack" },
+ { 306, "Dot1ag chassis - management info" },
+ { 307, "Octet string" },
+ { 308, "General purpose buffer" },
+ { 309, "Multicast operations profile" },
+ { 310, "Multicast subscriber config info" },
+ { 311, "Multicast subscriber monitor" },
+ { 312, "FEC performance monitoring history data" },
+ { 313, "RE ANI-G" },
+ { 314, "Physical path termination point RE UNI" },
+ { 315, "RE upstream amplifier" },
+ { 316, "RE downstream amplifier" },
+ { 317, "RE config portal" },
+ { 318, "File transfer controller" },
+ { 319, "CES physical interface performance monitoring history data 2" },
+ { 320, "CES physical interface performance monitoring history data 3" },
+ { 321, "Ethernet frame performance monitoring history data downstream" },
+ { 322, "Ethernet frame performance monitoring history data upstream" },
+ { 323, "VDSL2 line configuration extensions 2" },
+ { 324, "xDSL impulse noise monitor performance monitoring history data" },
+ { 325, "xDSL line inventory and status data part 5" },
+ { 326, "xDSL line inventory and status data part 6" },
+ { 327, "xDSL line inventory and status data part 7" },
+ { 328, "RE common amplifier parameters" },
+ { 329, "Virtual Ethernet interface point" },
+ { 330, "Generic status portal" },
+ { 331, "ONU-E" },
+ { 332, "Enhanced security control" },
+ { 333, "MPLS pseudowire termination point" },
+ { 334, "Ethernet frame extended PM" },
+ { 335, "Simple network management protocol (SNMP) configuration data" },
+ { 336, "ONU dynamic power management control" },
+ { 337, "PW ATM configuration data" },
+ { 338, "PW ATM performance monitoring history data" },
+ { 339, "PW Ethernet configuration data" },
+ { 340, "BBF TR-069 management server" },
+ { 341, "GEM port network CTP performance monitoring history data" },
+ { 342, "TCP/UDP performance monitoring history data" },
+ { 343, "Energy consumption performance monitoring history data" },
+ { 344, "XG-PON TC performance monitoring history data" },
+ { 345, "XG-PON downstream management performance monitoring history data" },
+ { 346, "XG-PON upstream management performance monitoring history data" },
+ { 347, "IPv6 host config data" },
+ { 348, "MAC bridge port ICMPv6 process pre-assign table" },
+ { 349, "Power over Ethernet (PoE) control" },
+ { 400, "Ethernet pseudowire parameters" },
+ { 401, "Physical path termination point RS232/RS485 UNI" },
+ { 402, "RS232/RS485 port operation configuration data" },
+ { 403, "RS232/RS485 performance monitoring history data" },
+ { 404, "L2 multicast GEM interworking termination point" },
+ { 405, "ANI-E" },
+ { 406, "EPON downstream performance monitoring configuration" },
+ { 407, "SIP agent config data 2" },
+ { 408, "xDSL xTU-C performance monitoring history data part 2" },
+ { 409, "PTM performance monitoring history data xDSL" },
+ { 410, "VDSL2 line configuration extensions 3" },
+ { 411, "Vectoring line configuration extensions" },
+ { 412, "xDSL channel configuration profile part 2" },
+ { 413, "xTU data gathering configuration" },
+ { 414, "xDSL line inventory and status data part 8" },
+ { 415, "VDSL2 line inventory and status data part 4" },
+ { 416, "Vectoring line inventory and status data" },
+ { 417, "Data gathering line test, diagnostic and status" },
+ { 419, "EFM bonding group" },
+ { 420, "EFM bonding link" },
+ { 421, "EFM bonding group performance monitoring history data" },
+ { 422, "EFM bonding group performance monitoring history data part 2" },
+ { 423, "EFM bonding link performance monitoring history data" },
+ { 424, "EFM bonding port performance monitoring history data" },
+ { 425, "EFM bonding port performance monitoring history data part 2" },
+ { 426, "Ethernet frame extended PM 64 bit" },
+ { 427, "Physical path termination point xDSL UNI part 3" },
+ { 428, "FAST line configuration profile part 1" },
+ { 429, "FAST line configuration profile part 2" },
+ { 430, "FAST line configuration profile part 3" },
+ { 431, "FAST line configuration profile part 4" },
+ { 432, "FAST channel configuration profile" },
+ { 433, "FAST data path configuration profile" },
+ { 434, "FAST vectoring line configuration extensions" },
+ { 435, "FAST line inventory and status data" },
+ { 436, "FAST line inventory and status data part 2" },
+ { 437, "FAST xTU-C performance monitoring history data" },
+ { 438, "FAST xTU-R performance monitoring history data" },
+ { 439, "OpenFlow config data" },
+ { 440, "Time Status Message" },
+ { 441, "ONU3-G" },
+ { 442, "TWDM System Profile managed entity" },
+ { 443, "TWDM channel managed entity" },
+ { 444, "TWDM channel PHY/LODS performance monitoring history data" },
+ { 445, "TWDM channel XGEM performance monitoring history data" },
+ { 446, "TWDM channel PLOAM performance monitoring history data part 1" },
+ { 447, "TWDM channel PLOAM performance monitoring history data part 2" },
+ { 448, "TWDM channel PLOAM performance monitoring history data part 3" },
+ { 449, "TWDM channel tuning performance monitoring history data part 1" },
+ { 450, "TWDM channel tuning performance monitoring history data part 2" },
+ { 451, "TWDM channel tuning performance monitoring history data part 3" },
+ { 452, "TWDM channel OMCI performance monitoring history data" },
+};
+
+#define OMCI_ATTR(_bit, _offset, _len, _access) \
+ { .mask = BIT(_bit), .offset = (_offset), .len = (_len), \
+  .access = (_access) }
+
+#define OMCI_RW_SC (OMCI_ATTR_ACCESS_READ | OMCI_ATTR_ACCESS_WRITE | \
+ OMCI_ATTR_ACCESS_SET_CREATE)
+#define OMCI_RW (OMCI_ATTR_ACCESS_READ | OMCI_ATTR_ACCESS_WRITE)
+#define OMCI_R OMCI_ATTR_ACCESS_READ
+
+static const struct omci_attr_desc omci_software_image_attrs[] = {
+ OMCI_ATTR(15, 0, OMCI_SOFTWARE_VERSION_LEN, OMCI_R),
+ OMCI_ATTR(14, 14, 1, OMCI_R),
+ OMCI_ATTR(13, 15, 1, OMCI_R),
+ OMCI_ATTR(12, 16, 1, OMCI_R),
+};
+
+static const struct omci_attr_desc omci_cardholder_attrs[] = {
+ OMCI_ATTR(15, 0, 1, OMCI_R),
+ OMCI_ATTR(14, 1, 1, OMCI_RW),
+ OMCI_ATTR(13, 2, 1, OMCI_RW),
+ OMCI_ATTR(12, 3, 20, OMCI_RW),
+ OMCI_ATTR(11, 23, 20, OMCI_RW),
+ OMCI_ATTR(10, 43, 2, OMCI_R),
+};
+
+static const struct omci_attr_desc omci_circuit_pack_attrs[] = {
+ OMCI_ATTR(15, 0, 1, OMCI_R),
+ OMCI_ATTR(14, 1, 1, OMCI_R),
+ OMCI_ATTR(13, 2, 8, OMCI_R),
+ OMCI_ATTR(12, 10, 14, OMCI_R),
+ OMCI_ATTR(11, 24, 4, OMCI_R),
+ OMCI_ATTR(10, 28, 1, OMCI_RW),
+ OMCI_ATTR(9, 29, 1, OMCI_R),
+ OMCI_ATTR(8, 30, 1, OMCI_RW),
+ OMCI_ATTR(7, 31, 20, OMCI_R),
+ OMCI_ATTR(6, 51, 1, OMCI_RW),
+ OMCI_ATTR(5, 52, 1, OMCI_R),
+ OMCI_ATTR(4, 53, 1, OMCI_R),
+ OMCI_ATTR(3, 54, 1, OMCI_R),
+ OMCI_ATTR(2, 55, 4, OMCI_RW),
+};
+
+static const struct omci_attr_desc omci_pptp_ethernet_uni_attrs[] = {
+ OMCI_ATTR(15, 0, 1, OMCI_R),
+ OMCI_ATTR(14, 1, 1, OMCI_R),
+ OMCI_ATTR(13, 2, 1, OMCI_RW),
+ OMCI_ATTR(12, 3, 1, OMCI_RW),
+ OMCI_ATTR(11, 4, 1, OMCI_RW),
+ OMCI_ATTR(10, 5, 1, OMCI_R),
+ OMCI_ATTR(9, 6, 1, OMCI_R),
+ OMCI_ATTR(8, 7, 2, OMCI_RW),
+ OMCI_ATTR(7, 9, 1, OMCI_RW),
+ OMCI_ATTR(6, 10, 2, OMCI_RW),
+ OMCI_ATTR(5, 12, 1, OMCI_RW),
+ OMCI_ATTR(4, 13, 1, OMCI_RW),
+ OMCI_ATTR(3, 14, 1, OMCI_RW),
+ OMCI_ATTR(2, 15, 1, OMCI_RW),
+ OMCI_ATTR(1, 16, 1, OMCI_RW),
+};
+
+static const struct omci_attr_desc omci_bridge_port_attrs[] = {
+ OMCI_ATTR(15, 0, 2, OMCI_RW_SC),
+ OMCI_ATTR(14, 2, 1, OMCI_RW_SC),
+ OMCI_ATTR(13, 3, 1, OMCI_RW_SC),
+ OMCI_ATTR(12, 4, 2, OMCI_RW_SC),
+ OMCI_ATTR(11, 6, 2, OMCI_RW_SC),
+ OMCI_ATTR(10, 8, 2, OMCI_RW_SC),
+ OMCI_ATTR(9, 10, 1, OMCI_RW_SC),
+ OMCI_ATTR(8, 11, 1, OMCI_RW_SC),
+ OMCI_ATTR(7, 12, 1, OMCI_RW_SC),
+ OMCI_ATTR(6, 13, 6, OMCI_R),
+ OMCI_ATTR(5, 19, 2, OMCI_RW),
+ OMCI_ATTR(4, 21, 2, OMCI_RW),
+ OMCI_ATTR(3, 23, 1, OMCI_RW_SC),
+};
+
+static const struct omci_attr_desc omci_vlan_filter_attrs[] = {
+ OMCI_ATTR(15, 0, 24, OMCI_RW_SC),
+ OMCI_ATTR(14, 24, 1, OMCI_RW_SC),
+ OMCI_ATTR(13, 25, 1, OMCI_RW_SC),
+};
+
+static const struct omci_attr_desc omci_8021p_mapper_attrs[] = {
+ OMCI_ATTR(15, 0, 2, OMCI_RW_SC),
+ OMCI_ATTR(14, 2, 2, OMCI_RW_SC),
+ OMCI_ATTR(13, 4, 2, OMCI_RW_SC),
+ OMCI_ATTR(12, 6, 2, OMCI_RW_SC),
+ OMCI_ATTR(11, 8, 2, OMCI_RW_SC),
+ OMCI_ATTR(10, 10, 2, OMCI_RW_SC),
+ OMCI_ATTR(9, 12, 2, OMCI_RW_SC),
+ OMCI_ATTR(8, 14, 2, OMCI_RW_SC),
+ OMCI_ATTR(7, 16, 2, OMCI_RW_SC),
+ OMCI_ATTR(6, 18, 1, OMCI_RW_SC),
+ OMCI_ATTR(5, 19, 24, OMCI_RW),
+ OMCI_ATTR(4, 43, 1, OMCI_RW_SC),
+ OMCI_ATTR(3, 44, 1, OMCI_RW_SC),
+};
+
+static const struct omci_attr_desc omci_extended_vlan_attrs[] = {
+ OMCI_ATTR(15, 0, 1, OMCI_RW_SC),
+ OMCI_ATTR(14, 1, 2, OMCI_R),
+ OMCI_ATTR(13, 3, 2, OMCI_RW),
+ OMCI_ATTR(12, 5, 2, OMCI_RW),
+ OMCI_ATTR(11, 7, 1, OMCI_RW),
+ OMCI_ATTR(10, 8, 16, OMCI_RW),
+ OMCI_ATTR(9, 24, 2, OMCI_RW_SC),
+ OMCI_ATTR(8, 26, 24, OMCI_RW),
+};
+
+static const struct omci_attr_desc omci_tcont_attrs[] = {
+ OMCI_ATTR(15, 0, 2, OMCI_RW),
+ OMCI_ATTR(14, 2, 1, OMCI_R),
+ OMCI_ATTR(13, 3, 1, OMCI_RW),
+};
+
+static const struct omci_attr_desc omci_uni_g_attrs[] = {
+ OMCI_ATTR(15, 0, 2, OMCI_RW),
+ OMCI_ATTR(14, 2, 1, OMCI_RW),
+ OMCI_ATTR(13, 3, 1, OMCI_R),
+ OMCI_ATTR(12, 4, 2, OMCI_RW),
+};
+
+static const struct omci_attr_desc omci_gem_iwtp_attrs[] = {
+ OMCI_ATTR(15, 0, 2, OMCI_RW_SC),
+ OMCI_ATTR(14, 2, 1, OMCI_RW_SC),
+ OMCI_ATTR(13, 3, 2, OMCI_RW_SC),
+ OMCI_ATTR(12, 5, 2, OMCI_RW_SC),
+ OMCI_ATTR(11, 7, 1, OMCI_R),
+ OMCI_ATTR(10, 8, 1, OMCI_R),
+ OMCI_ATTR(9, 9, 2, OMCI_RW_SC),
+ OMCI_ATTR(8, 11, 1, OMCI_RW),
+};
+
+static const struct omci_attr_desc omci_gem_port_ctp_attrs[] = {
+ OMCI_ATTR(15, 0, 2, OMCI_RW_SC),
+ OMCI_ATTR(14, 2, 2, OMCI_RW_SC),
+ OMCI_ATTR(13, 4, 1, OMCI_RW_SC),
+ OMCI_ATTR(12, 5, 2, OMCI_RW_SC),
+ OMCI_ATTR(11, 7, 2, OMCI_RW_SC),
+ OMCI_ATTR(10, 9, 1, OMCI_R),
+ OMCI_ATTR(9, 10, 2, OMCI_RW_SC),
+ OMCI_ATTR(8, 12, 1, OMCI_R),
+ OMCI_ATTR(7, 13, 2, OMCI_RW_SC),
+ OMCI_ATTR(6, 15, 2, OMCI_RW),
+};
+
+static const struct omci_attr_desc omci_veip_attrs[] = {
+ OMCI_ATTR(15, 0, 1, OMCI_RW),
+ OMCI_ATTR(14, 1, 1, OMCI_R),
+ OMCI_ATTR(13, 2, 25, OMCI_RW),
+ OMCI_ATTR(12, 27, 2, OMCI_RW),
+ OMCI_ATTR(11, 29, 2, OMCI_R),
+};
+
+static const struct omci_attr_desc omci_huawei_350_attrs[] = {
+ OMCI_ATTR(15, 0, 1, OMCI_RW),
+ OMCI_ATTR(14, 1, 1, OMCI_R),
+ OMCI_ATTR(13, 2, 1, OMCI_RW),
+ OMCI_ATTR(12, 3, 1, OMCI_RW),
+ OMCI_ATTR(11, 4, 2, OMCI_R),
+ OMCI_ATTR(10, 6, 4, OMCI_RW),
+ OMCI_ATTR(9, 10, 16, OMCI_R),
+ OMCI_ATTR(8, 26, 4, OMCI_R),
+ OMCI_ATTR(7, 30, 6, OMCI_RW),
+};
+
+static const struct omci_attr_desc omci_huawei_352_attrs[] = {
+ OMCI_ATTR(15, 0, 2, OMCI_RW_SC),
+ OMCI_ATTR(14, 2, 2, OMCI_RW_SC),
+ OMCI_ATTR(13, 4, 2, OMCI_RW_SC),
+ OMCI_ATTR(12, 6, 1, OMCI_RW_SC),
+ OMCI_ATTR(11, 7, 4, OMCI_RW_SC),
+ OMCI_ATTR(10, 11, 4, OMCI_RW_SC),
+};
+
+static const struct omci_attr_desc omci_huawei_353_attrs[] = {
+ OMCI_ATTR(15, 0, 14, OMCI_R),
+ OMCI_ATTR(14, 14, 1, OMCI_R),
+ OMCI_ATTR(13, 15, 1, OMCI_R),
+ OMCI_ATTR(12, 16, 1, OMCI_R),
+ OMCI_ATTR(11, 17, 4, OMCI_RW),
+};
+
+static const struct omci_attr_desc omci_huawei_multicast_attrs[] = {
+ OMCI_ATTR(15, 0, 1, OMCI_RW),
+ OMCI_ATTR(14, 1, 2, OMCI_RW),
+ OMCI_ATTR(13, 3, 2, OMCI_RW),
+ OMCI_ATTR(12, 5, 2, OMCI_RW),
+ OMCI_ATTR(11, 7, 4, OMCI_RW),
+};
+
+static const struct omci_attr_desc omci_nokia_65295_attrs[] = {
+ OMCI_ATTR(15, 0, 1, OMCI_R),
+ OMCI_ATTR(14, 1, 2, OMCI_R),
+ OMCI_ATTR(13, 3, 1, OMCI_R),
+ OMCI_ATTR(12, 4, 2, OMCI_R),
+ OMCI_ATTR(11, 6, 1, OMCI_R),
+ OMCI_ATTR(10, 7, 2, OMCI_R),
+ OMCI_ATTR(9, 9, 1, OMCI_R),
+ OMCI_ATTR(8, 10, 2, OMCI_R),
+ OMCI_ATTR(7, 12, 1, OMCI_R),
+ OMCI_ATTR(6, 13, 2, OMCI_R),
+};
+
+static const struct omci_attr_desc omci_nokia_65296_attrs[] = {
+ OMCI_ATTR(15, 0, 1, OMCI_RW),
+ OMCI_ATTR(14, 1, 1, OMCI_RW),
+ OMCI_ATTR(13, 2, 4, OMCI_R),
+ OMCI_ATTR(12, 6, 1, OMCI_RW),
+ OMCI_ATTR(11, 7, 1, OMCI_RW),
+ OMCI_ATTR(10, 8, 1, OMCI_RW),
+ OMCI_ATTR(9, 9, 4, OMCI_R),
+ OMCI_ATTR(8, 13, 1, OMCI_R),
+ OMCI_ATTR(7, 14, 1, OMCI_RW),
+ OMCI_ATTR(6, 15, 12, OMCI_R),
+ OMCI_ATTR(5, 27, 4, OMCI_R),
+ OMCI_ATTR(4, 31, 25, OMCI_R),
+};
+
+static const struct omci_attr_desc omci_nokia_65297_attrs[] = {
+ OMCI_ATTR(15, 0, 1, OMCI_RW),
+ OMCI_ATTR(14, 1, 1, OMCI_RW),
+ OMCI_ATTR(13, 2, 2, OMCI_RW),
+ OMCI_ATTR(12, 4, 1, OMCI_RW),
+ OMCI_ATTR(11, 5, 1, OMCI_RW),
+ OMCI_ATTR(10, 6, 1, OMCI_RW),
+ OMCI_ATTR(9, 7, 1, OMCI_RW),
+};
+
+static const struct omci_attr_desc omci_nokia_65305_attrs[] = {
+ OMCI_ATTR(15, 0, 1, OMCI_RW),
+ OMCI_ATTR(14, 1, 6, OMCI_RW),
+};
+
+static const struct omci_attr_desc omci_nokia_65306_attrs[] = {
+ OMCI_ATTR(15, 0, 19, OMCI_RW),
+};
+
+static const struct omci_attr_desc omci_omci_attrs[] = {
+ OMCI_ATTR(15, 0, 4, OMCI_R),
+ OMCI_ATTR(14, 4, 4, OMCI_R),
+};
+
+static const struct omci_attr_desc omci_managed_entity_attrs[] = {
+ OMCI_ATTR(15, 0, 25, OMCI_R),
+ OMCI_ATTR(14, 25, 4, OMCI_R),
+ OMCI_ATTR(13, 29, 1, OMCI_R),
+ OMCI_ATTR(12, 30, 4, OMCI_R),
+ OMCI_ATTR(11, 34, 4, OMCI_R),
+ OMCI_ATTR(10, 38, 4, OMCI_R),
+ OMCI_ATTR(9, 42, 4, OMCI_R),
+ OMCI_ATTR(8, 46, 1, OMCI_R),
+};
+
+static const struct omci_attr_desc omci_attribute_attrs[] = {
+ OMCI_ATTR(15, 0, 25, OMCI_R),
+ OMCI_ATTR(14, 25, 2, OMCI_R),
+ OMCI_ATTR(13, 27, 1, OMCI_R),
+ OMCI_ATTR(12, 28, 1, OMCI_R),
+ OMCI_ATTR(11, 29, 4, OMCI_R),
+ OMCI_ATTR(10, 33, 4, OMCI_R),
+ OMCI_ATTR(9, 37, 4, OMCI_R),
+ OMCI_ATTR(8, 41, 4, OMCI_R),
+ OMCI_ATTR(7, 45, 1, OMCI_R),
+};
+
+#define STANDARD_ACTIONS (OMCI_ME_ACTION_GET | OMCI_ME_ACTION_SET)
+#define OLT_CREATED_ACTIONS (OMCI_ME_ACTION_CREATE | OMCI_ME_ACTION_DELETE | \
+     OMCI_ME_ACTION_GET | OMCI_ME_ACTION_SET)
+#define STANDARD_DESC(_class, _name, _actions, _flags, _mask, _upload, \
+      _len, _category, _support, _attrs) \
+ { .class_id = (_class), .name = (_name), \
+  .profile_mask = OMCI_PROFILE_STANDARD_MASK, .actions = (_actions), \
+  .flags = (_flags), .valid_attr_mask = (_mask), \
+  .mib_upload_mask = (_upload), .data_len = (_len), \
+  .category = (_category), .support = (_support), \
+  .attrs = (_attrs), .num_attrs = ARRAY_SIZE(_attrs) }
+#define VENDOR_DESC(_class, _name, _profile, _actions, _flags, _mask, \
+    _upload, _len, _category, _support, _attrs) \
+ { .class_id = (_class), .name = (_name), \
+  .profile_mask = OMCI_PROFILE_BIT(_profile), .actions = (_actions), \
+  .flags = (_flags) | OMCI_ME_F_VENDOR, .valid_attr_mask = (_mask), \
+  .mib_upload_mask = (_upload), .data_len = (_len), \
+  .category = (_category), .support = (_support), \
+  .attrs = (_attrs), .num_attrs = ARRAY_SIZE(_attrs) }
+
+static const struct omci_me_desc omci_me_descs[] = {
+ STANDARD_DESC(OMCI_CLASS_SOFTWARE_IMAGE, "Software image",
+      OMCI_ME_ACTION_GET, OMCI_ME_F_ONU_CREATED,
+      GENMASK(15, 12), GENMASK(15, 12), 17,
+      OMCI_CLASS_CATEGORY_EQUIPMENT, OMCI_CLASS_SUPPORT_NATIVE,
+      omci_software_image_attrs),
+ STANDARD_DESC(OMCI_CLASS_CARDHOLDER, "Cardholder", STANDARD_ACTIONS,
+      OMCI_ME_F_ONU_CREATED, GENMASK(15, 10), GENMASK(15, 10),
+      45, OMCI_CLASS_CATEGORY_EQUIPMENT,
+      OMCI_CLASS_SUPPORT_NATIVE, omci_cardholder_attrs),
+ STANDARD_DESC(OMCI_CLASS_CIRCUIT_PACK, "Circuit pack", STANDARD_ACTIONS,
+      OMCI_ME_F_ONU_CREATED, GENMASK(15, 2), GENMASK(15, 2),
+      59, OMCI_CLASS_CATEGORY_EQUIPMENT,
+      OMCI_CLASS_SUPPORT_NATIVE, omci_circuit_pack_attrs),
+ STANDARD_DESC(OMCI_CLASS_PPTP_ETHERNET_UNI,
+      "Physical path termination point Ethernet UNI",
+      STANDARD_ACTIONS, OMCI_ME_F_ONU_CREATED | OMCI_ME_F_DATAPATH,
+      GENMASK(15, 1), GENMASK(15, 1), 17,
+      OMCI_CLASS_CATEGORY_UNI, OMCI_CLASS_SUPPORT_PROVISIONED,
+      omci_pptp_ethernet_uni_attrs),
+ STANDARD_DESC(OMCI_CLASS_MAC_BRIDGE_PORT_CONFIG_DATA,
+      "MAC bridge port configuration data", OLT_CREATED_ACTIONS,
+      OMCI_ME_F_DATAPATH, GENMASK(15, 3), GENMASK(15, 3), 24,
+      OMCI_CLASS_CATEGORY_LAYER2, OMCI_CLASS_SUPPORT_PROVISIONED,
+      omci_bridge_port_attrs),
+ STANDARD_DESC(OMCI_CLASS_VLAN_TAGGING_FILTER_DATA,
+      "VLAN tagging filter data", OLT_CREATED_ACTIONS,
+      OMCI_ME_F_DATAPATH, GENMASK(15, 13), GENMASK(15, 13), 26,
+      OMCI_CLASS_CATEGORY_LAYER2, OMCI_CLASS_SUPPORT_PROVISIONED,
+      omci_vlan_filter_attrs),
+ STANDARD_DESC(OMCI_CLASS_8021P_MAPPER,
+      "802.1p mapper service profile", OLT_CREATED_ACTIONS,
+      OMCI_ME_F_DATAPATH, GENMASK(15, 3), GENMASK(15, 3), 45,
+      OMCI_CLASS_CATEGORY_LAYER2, OMCI_CLASS_SUPPORT_PROVISIONED,
+      omci_8021p_mapper_attrs),
+ STANDARD_DESC(OMCI_CLASS_EXTENDED_VLAN,
+      "Extended VLAN tagging operation configuration data",
+      OLT_CREATED_ACTIONS | OMCI_ME_ACTION_SET_TABLE,
+      OMCI_ME_F_DATAPATH, GENMASK(15, 8), GENMASK(15, 8), 50,
+      OMCI_CLASS_CATEGORY_LAYER2, OMCI_CLASS_SUPPORT_PROVISIONED,
+      omci_extended_vlan_attrs),
+ STANDARD_DESC(OMCI_CLASS_TCONT, "T-CONT", STANDARD_ACTIONS,
+      OMCI_ME_F_ONU_CREATED | OMCI_ME_F_DATAPATH,
+      GENMASK(15, 13), GENMASK(15, 13), 4,
+      OMCI_CLASS_CATEGORY_ANI, OMCI_CLASS_SUPPORT_NATIVE,
+      omci_tcont_attrs),
+ STANDARD_DESC(OMCI_CLASS_UNI_G, "UNI-G", STANDARD_ACTIONS,
+      OMCI_ME_F_ONU_CREATED, GENMASK(15, 12), GENMASK(15, 12),
+      6, OMCI_CLASS_CATEGORY_UNI, OMCI_CLASS_SUPPORT_NATIVE,
+      omci_uni_g_attrs),
+ STANDARD_DESC(OMCI_CLASS_GEM_IWTP,
+      "GEM interworking termination point", OLT_CREATED_ACTIONS,
+      OMCI_ME_F_DATAPATH, GENMASK(15, 8), GENMASK(15, 8), 12,
+      OMCI_CLASS_CATEGORY_ANI, OMCI_CLASS_SUPPORT_PROVISIONED,
+      omci_gem_iwtp_attrs),
+ STANDARD_DESC(OMCI_CLASS_GEM_PORT_CTP, "GEM port network CTP",
+      OLT_CREATED_ACTIONS, OMCI_ME_F_DATAPATH,
+      GENMASK(15, 6), GENMASK(15, 6), 17,
+      OMCI_CLASS_CATEGORY_ANI, OMCI_CLASS_SUPPORT_PROVISIONED,
+      omci_gem_port_ctp_attrs),
+ STANDARD_DESC(OMCI_CLASS_OMCI, "OMCI",
+      OMCI_ME_ACTION_GET | OMCI_ME_ACTION_GET_NEXT,
+      OMCI_ME_F_ONU_CREATED | OMCI_ME_F_NO_MIB_UPLOAD,
+      GENMASK(15, 14), 0, 8,
+      OMCI_CLASS_CATEGORY_MANAGEMENT, OMCI_CLASS_SUPPORT_NATIVE,
+      omci_omci_attrs),
+ STANDARD_DESC(OMCI_CLASS_MANAGED_ENTITY, "Managed entity",
+      OMCI_ME_ACTION_GET | OMCI_ME_ACTION_GET_NEXT,
+      OMCI_ME_F_ONU_CREATED | OMCI_ME_F_NO_MIB_UPLOAD,
+      GENMASK(15, 8), 0, 47,
+      OMCI_CLASS_CATEGORY_MANAGEMENT, OMCI_CLASS_SUPPORT_NATIVE,
+      omci_managed_entity_attrs),
+ STANDARD_DESC(OMCI_CLASS_ATTRIBUTE, "Attribute",
+      OMCI_ME_ACTION_GET | OMCI_ME_ACTION_GET_NEXT,
+      OMCI_ME_F_ONU_CREATED | OMCI_ME_F_NO_MIB_UPLOAD,
+      GENMASK(15, 7), 0, 46,
+      OMCI_CLASS_CATEGORY_MANAGEMENT, OMCI_CLASS_SUPPORT_NATIVE,
+      omci_attribute_attrs),
+ STANDARD_DESC(OMCI_CLASS_VEIP, "Virtual Ethernet interface point",
+      STANDARD_ACTIONS, OMCI_ME_F_ONU_CREATED | OMCI_ME_F_DATAPATH,
+      GENMASK(15, 11), GENMASK(15, 11), 31,
+      OMCI_CLASS_CATEGORY_UNI, OMCI_CLASS_SUPPORT_PROVISIONED,
+      omci_veip_attrs),
+
+ VENDOR_DESC(OMCI_CLASS_HUAWEI_FLOW_MAPPING,
+    "Huawei flow mapping configuration", OMCI_OLT_PROFILE_HUAWEI,
+    STANDARD_ACTIONS, OMCI_ME_F_ONU_CREATED,
+    GENMASK(15, 7), GENMASK(15, 7), 36,
+    OMCI_CLASS_CATEGORY_VENDOR, OMCI_CLASS_SUPPORT_NATIVE,
+    omci_huawei_350_attrs),
+ VENDOR_DESC(OMCI_CLASS_HUAWEI_VLAN_MAPPING,
+    "Huawei VLAN mapping configuration", OMCI_OLT_PROFILE_HUAWEI,
+    OLT_CREATED_ACTIONS, OMCI_ME_F_NO_MIB_UPLOAD | OMCI_ME_F_DATAPATH,
+    GENMASK(15, 10), 0, 15, OMCI_CLASS_CATEGORY_VENDOR,
+    OMCI_CLASS_SUPPORT_SHADOW, omci_huawei_352_attrs),
+ VENDOR_DESC(OMCI_CLASS_HUAWEI_SW_IMAGE_EXT,
+    "Huawei software image extension", OMCI_OLT_PROFILE_HUAWEI,
+    STANDARD_ACTIONS, OMCI_ME_F_ONU_CREATED,
+    GENMASK(15, 11), GENMASK(15, 11), 21,
+    OMCI_CLASS_CATEGORY_VENDOR, OMCI_CLASS_SUPPORT_NATIVE,
+    omci_huawei_353_attrs),
+#define HUAWEI_MULTICAST_DESC(_class, _name) \
+ VENDOR_DESC((_class), (_name), OMCI_OLT_PROFILE_HUAWEI, STANDARD_ACTIONS, \
+    OMCI_ME_F_ONU_CREATED | OMCI_ME_F_DATAPATH, GENMASK(15, 11), \
+    GENMASK(15, 11), 11, \
+    OMCI_CLASS_CATEGORY_MULTICAST, OMCI_CLASS_SUPPORT_SHADOW, \
+    omci_huawei_multicast_attrs)
+ HUAWEI_MULTICAST_DESC(OMCI_CLASS_HUAWEI_MULTICAST_367,
+      "Huawei multicast configuration 367"),
+ HUAWEI_MULTICAST_DESC(OMCI_CLASS_HUAWEI_MULTICAST_370,
+      "Huawei multicast configuration 370"),
+ HUAWEI_MULTICAST_DESC(OMCI_CLASS_HUAWEI_MULTICAST_373,
+      "Huawei multicast configuration 373"),
+ HUAWEI_MULTICAST_DESC(OMCI_CLASS_HUAWEI_MULTICAST_65408,
+      "Huawei multicast configuration 65408"),
+ HUAWEI_MULTICAST_DESC(OMCI_CLASS_HUAWEI_MULTICAST_65414,
+      "Huawei multicast configuration 65414"),
+ HUAWEI_MULTICAST_DESC(OMCI_CLASS_HUAWEI_MULTICAST_65425,
+      "Huawei multicast configuration 65425"),
+#undef HUAWEI_MULTICAST_DESC
+
+ VENDOR_DESC(OMCI_CLASS_NOKIA_OPTICAL_SUPERVISION,
+    "Nokia ONT optical supervision status",
+    OMCI_OLT_PROFILE_NOKIA_ALCL, OMCI_ME_ACTION_GET,
+    OMCI_ME_F_ONU_CREATED, GENMASK(15, 6), GENMASK(15, 6), 15,
+    OMCI_CLASS_CATEGORY_VENDOR, OMCI_CLASS_SUPPORT_NATIVE,
+    omci_nokia_65295_attrs),
+ VENDOR_DESC(OMCI_CLASS_NOKIA_ONT_GENERIC_V2,
+    "Nokia ONT generic V2", OMCI_OLT_PROFILE_NOKIA_ALCL,
+    STANDARD_ACTIONS, OMCI_ME_F_ONU_CREATED,
+    GENMASK(15, 4), GENMASK(15, 4), 56,
+    OMCI_CLASS_CATEGORY_VENDOR, OMCI_CLASS_SUPPORT_NATIVE,
+    omci_nokia_65296_attrs),
+ VENDOR_DESC(OMCI_CLASS_NOKIA_UNI_SUPPLEMENTAL_V2,
+    "Nokia UNI supplemental 1 V2", OMCI_OLT_PROFILE_NOKIA_ALCL,
+    STANDARD_ACTIONS, OMCI_ME_F_ONU_CREATED,
+    GENMASK(15, 9), GENMASK(15, 9), 8,
+    OMCI_CLASS_CATEGORY_VENDOR, OMCI_CLASS_SUPPORT_SHADOW,
+    omci_nokia_65297_attrs),
+ VENDOR_DESC(OMCI_CLASS_NOKIA_VLAN_DS_SUPPLEMENTAL,
+    "Nokia VLAN downstream supplemental",
+    OMCI_OLT_PROFILE_NOKIA_ALCL, OLT_CREATED_ACTIONS,
+    OMCI_ME_F_DATAPATH, GENMASK(15, 14), GENMASK(15, 14), 7,
+    OMCI_CLASS_CATEGORY_VENDOR, OMCI_CLASS_SUPPORT_SHADOW,
+    omci_nokia_65305_attrs),
+ VENDOR_DESC(OMCI_CLASS_NOKIA_VLAN_US_POLICER,
+    "Nokia upstream VLAN policer", OMCI_OLT_PROFILE_NOKIA_ALCL,
+    OLT_CREATED_ACTIONS, OMCI_ME_F_DATAPATH,
+    BIT(15), BIT(15), 19, OMCI_CLASS_CATEGORY_VENDOR,
+    OMCI_CLASS_SUPPORT_SHADOW, omci_nokia_65306_attrs),
+};
+
+static bool omci_class_name_contains(const char *name, const char *needle)
+{
+ return strnstr(name, needle, strlen(name));
+}
+
+static u8 omci_class_category(u16 class_id, const char *name)
+{
+ if ((class_id >= 240 && class_id <= 255) ||
+    (class_id >= 350 && class_id <= 399) || class_id >= 65280)
+ return OMCI_CLASS_CATEGORY_VENDOR;
+ if ((class_id >= 172 && class_id <= 239) ||
+    (class_id >= 453 && class_id <= 65279) ||
+    omci_class_name_contains(name, "Reserved") ||
+    omci_class_name_contains(name, "Intentionally left blank"))
+ return OMCI_CLASS_CATEGORY_RESERVED;
+ if (omci_class_name_contains(name, "performance monitoring") ||
+    omci_class_name_contains(name, "PM history") ||
+    omci_class_name_contains(name, "monitoring history"))
+ return OMCI_CLASS_CATEGORY_PERFORMANCE;
+ if (omci_class_name_contains(name, "xDSL") ||
+    omci_class_name_contains(name, "VDSL") ||
+    omci_class_name_contains(name, "FAST") ||
+    omci_class_name_contains(name, "EFM bonding"))
+ return OMCI_CLASS_CATEGORY_XDSL;
+ if (omci_class_name_contains(name, "VoIP") ||
+    omci_class_name_contains(name, "SIP") ||
+    omci_class_name_contains(name, "MGC") ||
+    omci_class_name_contains(name, "RTP") ||
+    omci_class_name_contains(name, "Voice") ||
+    omci_class_name_contains(name, "POTS"))
+ return OMCI_CLASS_CATEGORY_VOICE;
+ if (omci_class_name_contains(name, "Multicast"))
+ return OMCI_CLASS_CATEGORY_MULTICAST;
+ if (omci_class_name_contains(name, "security") ||
+    omci_class_name_contains(name, "Authentication") ||
+    omci_class_name_contains(name, "Dot1X") ||
+    omci_class_name_contains(name, "Radius"))
+ return OMCI_CLASS_CATEGORY_SECURITY;
+ if (omci_class_name_contains(name, "IP ") ||
+    omci_class_name_contains(name, "TCP/UDP") ||
+    omci_class_name_contains(name, "ARP") ||
+    omci_class_name_contains(name, "ICMP") ||
+    omci_class_name_contains(name, "TR-069") ||
+    omci_class_name_contains(name, "SNMP") ||
+    omci_class_name_contains(name, "Network address"))
+ return OMCI_CLASS_CATEGORY_LAYER3;
+ if (omci_class_name_contains(name, "VLAN") ||
+    omci_class_name_contains(name, "MAC bridge") ||
+    omci_class_name_contains(name, "Ethernet") ||
+    omci_class_name_contains(name, "802.1p") ||
+    omci_class_name_contains(name, "Dot1ag") ||
+    omci_class_name_contains(name, "OpenFlow"))
+ return OMCI_CLASS_CATEGORY_LAYER2;
+ if (omci_class_name_contains(name, "UNI"))
+ return OMCI_CLASS_CATEGORY_UNI;
+ if (omci_class_name_contains(name, "ANI") ||
+    omci_class_name_contains(name, "GEM") ||
+    omci_class_name_contains(name, "T-CONT") ||
+    omci_class_name_contains(name, "PON") ||
+    omci_class_name_contains(name, "Traffic scheduler") ||
+    omci_class_name_contains(name, "Priority queue"))
+ return OMCI_CLASS_CATEGORY_ANI;
+ if (class_id <= 7 || class_id == 133 || class_id == 159 ||
+    class_id == 160 || class_id == 256 || class_id == 257 ||
+    class_id == 297 || class_id == 331 || class_id == 336 ||
+    class_id == 441)
+ return OMCI_CLASS_CATEGORY_EQUIPMENT;
+ if (class_id == 137 || class_id == 157 || class_id == 158 ||
+    class_id == 287 || class_id == 288 || class_id == 289 ||
+    class_id == 307 || class_id == 308 || class_id == 318 ||
+    class_id == 330 || class_id == 440)
+ return OMCI_CLASS_CATEGORY_MANAGEMENT;
+ if (class_id < 256)
+ return OMCI_CLASS_CATEGORY_LEGACY;
+
+ return OMCI_CLASS_CATEGORY_OTHER;
+}
+
+static u32 omci_class_flags(u16 class_id, const char *name)
+{
+ u32 flags = OMCI_CLASS_F_STANDARD;
+ u8 category = omci_class_category(class_id, name);
+
+ if (omci_class_name_contains(name, "deprecated") ||
+    omci_class_name_contains(name, "obsolete"))
+ flags |= OMCI_CLASS_F_DEPRECATED;
+ if (category == OMCI_CLASS_CATEGORY_RESERVED)
+ flags |= OMCI_CLASS_F_RESERVED;
+ if (category == OMCI_CLASS_CATEGORY_VENDOR)
+ flags |= OMCI_CLASS_F_VENDOR_SPECIFIC;
+ if (category == OMCI_CLASS_CATEGORY_PERFORMANCE)
+ flags |= OMCI_CLASS_F_PERFORMANCE;
+ if (omci_class_name_contains(name, "table") || class_id == 84 ||
+    class_id == 145 || class_id == 171)
+ flags |= OMCI_CLASS_F_TABLE;
+
+ return flags;
+}
+
+const struct omci_me_desc *omci_me_lookup_profile(u8 profile, u16 class_id)
+{
+ unsigned int i;
+
+ if (profile == OMCI_OLT_PROFILE_UNSPEC ||
+    profile == OMCI_OLT_PROFILE_AUTO)
+ profile = OMCI_OLT_PROFILE_GENERIC;
+
+ for (i = 0; i < ARRAY_SIZE(omci_me_descs); i++) {
+ const struct omci_me_desc *desc = &omci_me_descs[i];
+
+ if (desc->class_id == class_id &&
+    (desc->profile_mask & OMCI_PROFILE_BIT(profile)))
+ return desc;
+ }
+
+ return NULL;
+}
+
+const struct omci_me_desc *omci_me_lookup(const struct omci_agent *agent,
+  u16 class_id)
+{
+ return omci_me_lookup_profile(agent->profile_effective, class_id);
+}
+
+bool omci_me_active(const struct omci_agent *agent, u16 class_id)
+{
+ const struct omci_me_desc *desc;
+
+ desc = omci_me_lookup(agent, class_id);
+ if (desc)
+ return true;
+
+ /* Standard catalogue entries remain available as shadow objects. */
+ return class_id < 240 || (class_id >= 256 && class_id < 350) ||
+       (class_id >= 400 && class_id <= 452);
+}
+
+bool omci_me_action_allowed(const struct omci_agent *agent, u16 class_id,
+    u8 message_type)
+{
+ const struct omci_me_desc *desc = omci_me_lookup(agent, class_id);
+
+ if (!desc)
+ return omci_me_active(agent, class_id);
+ if (message_type >= 32)
+ return false;
+
+ return !!(desc->actions & BIT(message_type));
+}
+
+static const struct omci_attr_desc *
+omci_me_find_attr(const struct omci_me_desc *desc, u16 mask)
+{
+ unsigned int i;
+
+ for (i = 0; i < desc->num_attrs; i++)
+ if (desc->attrs[i].mask == mask)
+ return &desc->attrs[i];
+
+ return NULL;
+}
+
+int omci_me_decode_create(const struct omci_me_desc *desc,
+  struct omci_mib_object *object,
+  const u8 *data, size_t len)
+{
+ unsigned int i;
+ u16 mask = 0;
+
+ if (!desc)
+ return 0;
+ for (i = 0; i < desc->num_attrs; i++) {
+ const struct omci_attr_desc *attr = &desc->attrs[i];
+
+ if (!(attr->access & OMCI_ATTR_ACCESS_SET_CREATE))
+ continue;
+ if (attr->offset + attr->len > sizeof(object->data) ||
+    len < attr->len)
+ return -EINVAL;
+ memcpy(object->data + attr->offset, data, attr->len);
+ data += attr->len;
+ len -= attr->len;
+ mask |= attr->mask;
+ }
+ object->attr_mask |= mask;
+
+ return 0;
+}
+
+int omci_me_decode_set(const struct omci_me_desc *desc,
+       struct omci_mib_object *object, u16 mask,
+       const u8 *data, size_t len)
+{
+ unsigned int bit;
+
+ if (!desc)
+ return 0;
+ if (mask & ~desc->valid_attr_mask)
+ return -EINVAL;
+
+ for (bit = 0; bit < 16; bit++) {
+ const struct omci_attr_desc *attr;
+ u16 attr_mask = BIT(15 - bit);
+
+ if (!(mask & attr_mask))
+ continue;
+ attr = omci_me_find_attr(desc, attr_mask);
+ if (!attr || !(attr->access & OMCI_ATTR_ACCESS_WRITE))
+ return -EACCES;
+ if (attr->offset + attr->len > sizeof(object->data) ||
+    len < attr->len)
+ return -EINVAL;
+ memcpy(object->data + attr->offset, data, attr->len);
+ data += attr->len;
+ len -= attr->len;
+ }
+ object->attr_mask |= mask;
+
+ return 0;
+}
+
+int omci_me_encode_attributes(const struct omci_me_desc *desc,
+      const struct omci_mib_object *object,
+      u16 mask, u8 *data, size_t len,
+      u16 *encoded_mask, size_t *encoded_len)
+{
+ size_t used = 0;
+ unsigned int bit;
+ u16 encoded = 0;
+
+ if (!desc)
+ return -ENOENT;
+ mask &= desc->valid_attr_mask;
+ for (bit = 0; bit < 16; bit++) {
+ const struct omci_attr_desc *attr;
+ u16 attr_mask = BIT(15 - bit);
+
+ if (!(mask & attr_mask))
+ continue;
+ attr = omci_me_find_attr(desc, attr_mask);
+ if (!attr || !(attr->access & OMCI_ATTR_ACCESS_READ))
+ continue;
+ if (attr->offset + attr->len > sizeof(object->data))
+ return -EINVAL;
+ if (len - used < attr->len)
+ break;
+ memcpy(data + used, object->data + attr->offset, attr->len);
+ used += attr->len;
+ encoded |= attr_mask;
+ }
+ if (encoded_mask)
+ *encoded_mask = encoded;
+ if (encoded_len)
+ *encoded_len = used;
+
+ return encoded ? 0 : -ENOSPC;
+}
+
+unsigned int omci_me_attr_count(const struct omci_agent *agent)
+{
+ unsigned int count = 0;
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(omci_me_descs); i++) {
+ const struct omci_me_desc *desc = &omci_me_descs[i];
+
+ if (omci_me_lookup(agent, desc->class_id) != desc)
+ continue;
+ count += desc->num_attrs;
+ }
+
+ return count;
+}
+
+int omci_me_attr_id(const struct omci_agent *agent, u16 class_id,
+    unsigned int attr_index, u16 *attr_id)
+{
+ unsigned int next_id = 1;
+ unsigned int i;
+
+ if (!agent || !attr_id)
+ return -EINVAL;
+
+ for (i = 0; i < ARRAY_SIZE(omci_me_descs); i++) {
+ const struct omci_me_desc *desc = &omci_me_descs[i];
+
+ if (omci_me_lookup(agent, desc->class_id) != desc)
+ continue;
+ if (desc->class_id == class_id) {
+ if (attr_index >= desc->num_attrs)
+ return -ENOENT;
+ if (next_id + attr_index > U16_MAX)
+ return -EOVERFLOW;
+ *attr_id = next_id + attr_index;
+ return 0;
+ }
+ if (next_id + desc->num_attrs > U16_MAX)
+ return -EOVERFLOW;
+ next_id += desc->num_attrs;
+ }
+
+ return -ENOENT;
+}
+
+int omci_me_attr_get(const struct omci_agent *agent, u16 attr_id,
+     u16 *class_id, unsigned int *attr_index,
+     const struct omci_me_desc **me_desc,
+     const struct omci_attr_desc **me_attr)
+{
+ unsigned int next_id = 1;
+ unsigned int i;
+
+ if (!agent || !attr_id)
+ return -EINVAL;
+
+ for (i = 0; i < ARRAY_SIZE(omci_me_descs); i++) {
+ const struct omci_me_desc *desc = &omci_me_descs[i];
+ unsigned int index;
+
+ if (omci_me_lookup(agent, desc->class_id) != desc)
+ continue;
+ if (attr_id < next_id || attr_id >= next_id + desc->num_attrs) {
+ next_id += desc->num_attrs;
+ continue;
+ }
+
+ index = attr_id - next_id;
+ if (class_id)
+ *class_id = desc->class_id;
+ if (attr_index)
+ *attr_index = index;
+ if (me_desc)
+ *me_desc = desc;
+ if (me_attr)
+ *me_attr = &desc->attrs[index];
+ return 0;
+ }
+
+ return -ENOENT;
+}
+
+const char *omci_me_class_name(u16 class_id)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(omci_me_descs); i++)
+ if (omci_me_descs[i].class_id == class_id)
+ return omci_me_descs[i].name;
+ for (i = 0; i < ARRAY_SIZE(omci_classes); i++)
+ if (omci_classes[i].class_id == class_id)
+ return omci_classes[i].name;
+
+ if (class_id >= 172 && class_id <= 239)
+ return "Reserved for future managed entities";
+ if (class_id >= 240 && class_id <= 255)
+ return "Reserved for vendor-specific managed entities";
+ if (class_id >= 350 && class_id <= 399)
+ return "Vendor-specific managed entity";
+ if (class_id >= 453 && class_id <= 65279)
+ return "Reserved for future standardization";
+ if (class_id >= 65280)
+ return "Vendor-specific managed entity";
+
+ return "Unassigned managed entity";
+}
+
+int omci_me_class_get(struct omci_device *odev, u16 class_id,
+      struct omci_me_class *class)
+{
+ const struct omci_me_desc *desc;
+ const char *name;
+
+ if (!odev || !class)
+ return -EINVAL;
+ if (!omci_me_active(&odev->agent, class_id))
+ return -ENOENT;
+
+ desc = omci_me_lookup(&odev->agent, class_id);
+ name = omci_me_class_name(class_id);
+ class->class_id = class_id;
+ class->category = desc ? desc->category :
+  omci_class_category(class_id, name);
+ class->support = desc ? desc->support : OMCI_CLASS_SUPPORT_SHADOW;
+ class->flags = omci_class_flags(class_id, name);
+ if (desc) {
+ if (desc->flags & OMCI_ME_F_VENDOR)
+ class->flags |= OMCI_CLASS_F_VENDOR_SPECIFIC;
+ if (desc->flags & OMCI_ME_F_DATAPATH)
+ class->flags |= OMCI_CLASS_F_DATAPATH;
+ }
+ class->name = name;
+
+ return 0;
+}
+
+int omci_me_class_next(struct omci_device *odev, u32 index,
+       struct omci_me_class *class, u32 *next_index)
+{
+ u32 position = 0;
+ unsigned int i;
+
+ if (!odev || !class || !next_index)
+ return -EINVAL;
+
+ for (i = 0; i < ARRAY_SIZE(omci_classes); i++) {
+ if (!omci_me_active(&odev->agent, omci_classes[i].class_id))
+ continue;
+ if (position++ < index)
+ continue;
+ omci_me_class_get(odev, omci_classes[i].class_id, class);
+ *next_index = index + 1;
+ return 0;
+ }
+ for (i = 0; i < ARRAY_SIZE(omci_me_descs); i++) {
+ u16 class_id = omci_me_descs[i].class_id;
+ unsigned int j;
+ bool catalogued = false;
+
+ for (j = 0; j < ARRAY_SIZE(omci_classes); j++)
+ if (omci_classes[j].class_id == class_id) {
+ catalogued = true;
+ break;
+ }
+ if (catalogued || !omci_me_lookup(&odev->agent, class_id))
+ continue;
+ if (position++ < index)
+ continue;
+ omci_me_class_get(odev, class_id, class);
+ *next_index = index + 1;
+ return 0;
+ }
+
+ return -ENOENT;
+}
diff --git a/net/xpon/omci/me.h b/net/xpon/omci/me.h
new file mode 100644
index 000000000000..7953efcb9eef
--- /dev/null
+++ b/net/xpon/omci/me.h
@@ -0,0 +1,161 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _NET_OMCI_ME_H
+#define _NET_OMCI_ME_H
+
+#include <linux/bits.h>
+#include <linux/types.h>
+
+struct omci_agent;
+struct omci_device;
+struct omci_me_class;
+struct omci_mib_object;
+
+#define OMCI_CLASS_ONU_DATA 2
+#define OMCI_CLASS_CARDHOLDER 5
+#define OMCI_CLASS_CIRCUIT_PACK 6
+#define OMCI_CLASS_SOFTWARE_IMAGE 7
+#define OMCI_CLASS_PPTP_ETHERNET_UNI 11
+#define OMCI_CLASS_MAC_BRIDGE_SERVICE_PROFILE 45
+#define OMCI_CLASS_MAC_BRIDGE_CONFIG_DATA 46
+#define OMCI_CLASS_MAC_BRIDGE_PORT_CONFIG_DATA 47
+#define OMCI_CLASS_VLAN_TAGGING_FILTER_DATA 84
+#define OMCI_CLASS_8021P_MAPPER 130
+
+/*
+ * 802.1p mapper instances seeded for the Nokia/ALCL profile. One per GEM
+ * interworking termination point: mapper 1 fronts GEM 256 (management and
+ * VoIP), mapper 2 fronts GEM 257 (high speed internet).
+ */
+#define OMCI_NOKIA_MAPPER_MGMT 1
+#define OMCI_NOKIA_MAPPER_HSI 2
+#define OMCI_CLASS_OLT_G 131
+#define OMCI_CLASS_NETWORK_ADDRESS 137
+#define OMCI_CLASS_AUTH_METHOD 148
+#define OMCI_CLASS_EXTENDED_VLAN 171
+#define OMCI_CLASS_ONU_G 256
+#define OMCI_CLASS_ONU2_G 257
+
+/*
+ * ONU2-G security capability and security mode, G.988 9.1.3: 1 selects
+ * AES-128, the only cipher GPON defines.
+ */
+#define OMCI_ONU2G_SECURITY_AES128 1
+
+/*
+ * ONU2-G connectivity capability, G.988 Table 9.1.3-1. One bit per forwarding
+ * model: N:1 bridging, 1:M mapping, 1:P filtering, N:M bridge-mapping,
+ * 1:MP map-filtering, N:P bridge-filtering and N:MP bridge-map-filtering.
+ * The agent builds its datapath out of MAC bridges, GEM interworking TPs and
+ * a VEIP, so advertise the full set the way a stock ONU does and let the OLT
+ * pick.
+ */
+#define OMCI_ONU2G_CONNECTIVITY_CAPABILITY 0x007f
+#define OMCI_CLASS_TCONT 262
+#define OMCI_CLASS_ANI_G 263
+#define OMCI_CLASS_UNI_G 264
+#define OMCI_CLASS_GEM_IWTP 266
+#define OMCI_CLASS_GEM_PORT_CTP 268
+#define OMCI_CLASS_GAL_ETHERNET_PROFILE 272
+#define OMCI_CLASS_PRIORITY_QUEUE 277
+#define OMCI_CLASS_TRAFFIC_SCHEDULER 278
+#define OMCI_CLASS_OMCI 287
+#define OMCI_CLASS_MANAGED_ENTITY 288
+#define OMCI_CLASS_ATTRIBUTE 289
+#define OMCI_CLASS_VEIP 329
+
+#define OMCI_CLASS_HUAWEI_FLOW_MAPPING 350
+#define OMCI_CLASS_HUAWEI_VLAN_MAPPING 352
+#define OMCI_CLASS_HUAWEI_SW_IMAGE_EXT 353
+#define OMCI_CLASS_HUAWEI_MULTICAST_367 367
+#define OMCI_CLASS_HUAWEI_MULTICAST_370 370
+#define OMCI_CLASS_HUAWEI_MULTICAST_373 373
+#define OMCI_CLASS_HUAWEI_MULTICAST_65408 65408
+#define OMCI_CLASS_HUAWEI_MULTICAST_65414 65414
+#define OMCI_CLASS_HUAWEI_MULTICAST_65425 65425
+
+#define OMCI_CLASS_NOKIA_OPTICAL_SUPERVISION 65295
+#define OMCI_CLASS_NOKIA_ONT_GENERIC_V2 65296
+#define OMCI_CLASS_NOKIA_UNI_SUPPLEMENTAL_V2 65297
+#define OMCI_CLASS_NOKIA_GEM_PM_PART2 65300
+#define OMCI_CLASS_NOKIA_VLAN_DS_SUPPLEMENTAL 65305
+#define OMCI_CLASS_NOKIA_VLAN_US_POLICER 65306
+
+#define OMCI_ME_ACTION_CREATE BIT(4)
+#define OMCI_ME_ACTION_DELETE BIT(6)
+#define OMCI_ME_ACTION_SET BIT(8)
+#define OMCI_ME_ACTION_GET BIT(9)
+#define OMCI_ME_ACTION_GET_NEXT BIT(26)
+#define OMCI_ME_ACTION_MIB_UPLOAD BIT(13)
+#define OMCI_ME_ACTION_MIB_UPLOAD_NEXT BIT(14)
+#define OMCI_ME_ACTION_MIB_RESET BIT(15)
+#define OMCI_ME_ACTION_SET_TABLE BIT(29)
+
+#define OMCI_ME_F_NO_MIB_UPLOAD BIT(0)
+#define OMCI_ME_F_ONU_CREATED BIT(1)
+#define OMCI_ME_F_DATAPATH BIT(2)
+#define OMCI_ME_F_SAFE_FAKE BIT(3)
+#define OMCI_ME_F_VENDOR BIT(4)
+
+#define OMCI_PROFILE_BIT(_profile) BIT(_profile)
+#define OMCI_PROFILE_STANDARD_MASK GENMASK(7, 1)
+
+enum omci_attr_access {
+ OMCI_ATTR_ACCESS_READ = BIT(0),
+ OMCI_ATTR_ACCESS_WRITE = BIT(1),
+ OMCI_ATTR_ACCESS_SET_CREATE = BIT(2),
+};
+
+struct omci_attr_desc {
+ u16 mask;
+ u8 offset;
+ u8 len;
+ u8 access;
+};
+
+struct omci_me_desc {
+ u16 class_id;
+ const char *name;
+ u32 profile_mask;
+ u32 actions;
+ u32 flags;
+ u16 valid_attr_mask;
+ u16 mib_upload_mask;
+ u8 data_len;
+ u8 category;
+ u8 support;
+ const struct omci_attr_desc *attrs;
+ unsigned int num_attrs;
+};
+
+const struct omci_me_desc *omci_me_lookup(const struct omci_agent *agent,
+  u16 class_id);
+const struct omci_me_desc *omci_me_lookup_profile(u8 profile, u16 class_id);
+bool omci_me_active(const struct omci_agent *agent, u16 class_id);
+bool omci_me_action_allowed(const struct omci_agent *agent, u16 class_id,
+    u8 message_type);
+
+int omci_me_decode_create(const struct omci_me_desc *desc,
+  struct omci_mib_object *object,
+  const u8 *data, size_t len);
+int omci_me_decode_set(const struct omci_me_desc *desc,
+       struct omci_mib_object *object, u16 mask,
+       const u8 *data, size_t len);
+int omci_me_encode_attributes(const struct omci_me_desc *desc,
+      const struct omci_mib_object *object,
+      u16 mask, u8 *data, size_t len,
+      u16 *encoded_mask, size_t *encoded_len);
+
+const char *omci_me_class_name(u16 class_id);
+int omci_me_class_get(struct omci_device *odev, u16 class_id,
+      struct omci_me_class *class);
+int omci_me_class_next(struct omci_device *odev, u32 index,
+       struct omci_me_class *class, u32 *next_index);
+int omci_me_attr_id(const struct omci_agent *agent, u16 class_id,
+    unsigned int attr_index, u16 *attr_id);
+int omci_me_attr_get(const struct omci_agent *agent, u16 attr_id,
+     u16 *class_id, unsigned int *attr_index,
+     const struct omci_me_desc **desc,
+     const struct omci_attr_desc **attr);
+unsigned int omci_me_attr_count(const struct omci_agent *agent);
+
+#endif /* _NET_OMCI_ME_H */
diff --git a/net/xpon/omci/profiles.c b/net/xpon/omci/profiles.c
new file mode 100644
index 000000000000..9a3dc8c49b17
--- /dev/null
+++ b/net/xpon/omci/profiles.c
@@ -0,0 +1,225 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * OMCI OLT interoperability profiles
+ *
+ * Profiles describe policy deviations required by selected OLT families.
+ * Automatic detection is the default and uses OLT-G identification attributes
+ * only as a hint. Until OLT-G is available, the effective profile is generic.
+ */
+
+#include <linux/export.h>
+#include <linux/kernel.h>
+#include <linux/string.h>
+
+#include "internal.h"
+
+struct omci_profile_desc {
+ u8 profile;
+ u32 quirks;
+ const char *name;
+};
+
+static const struct omci_profile_desc omci_profiles[] = {
+ {
+ .profile = OMCI_OLT_PROFILE_GENERIC,
+ .name = "generic",
+ }, {
+ .profile = OMCI_OLT_PROFILE_AUTO,
+ .name = "auto",
+ }, {
+ .profile = OMCI_OLT_PROFILE_NOKIA_ALCL,
+ .quirks = OMCI_OLT_QUIRK_SANITIZE_VERSION |
+  OMCI_OLT_QUIRK_VENDOR_SPECIFIC_MES,
+ .name = "nokia-alcl",
+ }, {
+ .profile = OMCI_OLT_PROFILE_DASAN,
+ .quirks = OMCI_OLT_QUIRK_ALLOW_SET_CREATE |
+  OMCI_OLT_QUIRK_FAKE_UNSUPPORTED_SUCCESS |
+  OMCI_OLT_QUIRK_IGNORE_UNSUPPORTED_UNI |
+  OMCI_OLT_QUIRK_DASAN_MULTICAST_ANI,
+ .name = "dasan",
+ }, {
+ .profile = OMCI_OLT_PROFILE_HUAWEI,
+ .quirks = OMCI_OLT_QUIRK_VENDOR_SPECIFIC_MES,
+ .name = "huawei",
+ }, {
+ .profile = OMCI_OLT_PROFILE_FIBERHOME,
+ .quirks = OMCI_OLT_QUIRK_ALLOW_SET_CREATE |
+  OMCI_OLT_QUIRK_FULL_UNI_ENTITY_ID,
+ .name = "fiberhome",
+ }, {
+ .profile = OMCI_OLT_PROFILE_ZTE,
+ .quirks = OMCI_OLT_QUIRK_ALLOW_SET_CREATE |
+  OMCI_OLT_QUIRK_ZTE_VLAN_TAG_MODE,
+ .name = "zte",
+ },
+};
+
+static const struct omci_profile_desc *omci_profile_find(u8 profile)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(omci_profiles); i++)
+ if (omci_profiles[i].profile == profile)
+ return &omci_profiles[i];
+
+ return NULL;
+}
+
+bool omci_profile_valid(u8 profile)
+{
+ return !!omci_profile_find(profile);
+}
+
+bool omci_profile_forceable(u8 profile)
+{
+ return profile == OMCI_OLT_PROFILE_UNSPEC ||
+       (profile != OMCI_OLT_PROFILE_AUTO && omci_profile_valid(profile));
+}
+
+u32 omci_profile_quirks(u8 profile)
+{
+ const struct omci_profile_desc *desc = omci_profile_find(profile);
+
+ return desc ? desc->quirks : 0;
+}
+
+const char *omci_olt_profile_name(u8 profile)
+{
+ const struct omci_profile_desc *desc = omci_profile_find(profile);
+
+ return desc ? desc->name : "unspecified";
+}
+EXPORT_SYMBOL_GPL(omci_olt_profile_name);
+
+static bool omci_profile_vendor_is(const struct omci_olt_g *olt,
+   const char vendor[OMCI_OLT_VENDOR_ID_LEN])
+{
+ return olt && olt->vendor_id_valid &&
+       !strncasecmp(olt->vendor_id, vendor, OMCI_OLT_VENDOR_ID_LEN);
+}
+
+static bool omci_profile_field_contains(const char *field, size_t len,
+ const char *token)
+{
+ size_t token_len = strlen(token);
+ size_t i;
+
+ if (!token_len || token_len > len)
+ return false;
+
+ for (i = 0; i + token_len <= len && field[i]; i++)
+ if (!strncasecmp(field + i, token, token_len))
+ return true;
+
+ return false;
+}
+
+u8 omci_profile_detect(const struct omci_olt_g *olt)
+{
+ if (!olt || !olt->valid)
+ return OMCI_OLT_PROFILE_GENERIC;
+
+ if (omci_profile_vendor_is(olt, "ALCL") ||
+    omci_profile_vendor_is(olt, "NOKI"))
+ return OMCI_OLT_PROFILE_NOKIA_ALCL;
+ if (omci_profile_vendor_is(olt, "DSNW") ||
+    omci_profile_vendor_is(olt, "DASA"))
+ return OMCI_OLT_PROFILE_DASAN;
+ if (omci_profile_vendor_is(olt, "HWTC") ||
+    omci_profile_vendor_is(olt, "HWTI"))
+ return OMCI_OLT_PROFILE_HUAWEI;
+ if (omci_profile_vendor_is(olt, "FHTT") ||
+    omci_profile_vendor_is(olt, "FHTC"))
+ return OMCI_OLT_PROFILE_FIBERHOME;
+ if (omci_profile_vendor_is(olt, "ZTEG") ||
+    omci_profile_vendor_is(olt, "ZTEC"))
+ return OMCI_OLT_PROFILE_ZTE;
+
+ if (olt->equipment_id_valid) {
+ if (omci_profile_field_contains(olt->equipment_id,
+ OMCI_OLT_EQUIPMENT_ID_LEN,
+ "NOKIA") ||
+    omci_profile_field_contains(olt->equipment_id,
+ OMCI_OLT_EQUIPMENT_ID_LEN,
+ "ALCATEL"))
+ return OMCI_OLT_PROFILE_NOKIA_ALCL;
+ if (omci_profile_field_contains(olt->equipment_id,
+ OMCI_OLT_EQUIPMENT_ID_LEN,
+ "DASAN"))
+ return OMCI_OLT_PROFILE_DASAN;
+ if (omci_profile_field_contains(olt->equipment_id,
+ OMCI_OLT_EQUIPMENT_ID_LEN,
+ "HUAWEI"))
+ return OMCI_OLT_PROFILE_HUAWEI;
+ if (omci_profile_field_contains(olt->equipment_id,
+ OMCI_OLT_EQUIPMENT_ID_LEN,
+ "FIBERHOME"))
+ return OMCI_OLT_PROFILE_FIBERHOME;
+ if (omci_profile_field_contains(olt->equipment_id,
+ OMCI_OLT_EQUIPMENT_ID_LEN,
+ "ZTE"))
+ return OMCI_OLT_PROFILE_ZTE;
+ }
+
+ return OMCI_OLT_PROFILE_GENERIC;
+}
+
+void omci_profile_sanitize_olt_g(struct omci_olt_g *olt, u32 quirks)
+{
+ unsigned int i;
+
+ if (!olt || !(quirks & OMCI_OLT_QUIRK_SANITIZE_VERSION) ||
+    !olt->version_valid)
+ return;
+
+ for (i = 0; i < OMCI_OLT_VERSION_LEN && olt->version[i]; i++)
+ if (olt->version[i] < ' ' || olt->version[i] > '~')
+ olt->version[i] = ' ';
+}
+
+u16 omci_profile_normalize_uni_entity(u8 profile, u16 entity_id)
+{
+ if (omci_profile_quirks(profile) & OMCI_OLT_QUIRK_FULL_UNI_ENTITY_ID)
+ return entity_id;
+
+ return entity_id & 0xff;
+}
+
+void omci_profile_normalize_vlan_rule(u8 profile,
+      struct omci_extended_vlan_rule *rule)
+{
+ if (!rule || !(omci_profile_quirks(profile) &
+       OMCI_OLT_QUIRK_ZTE_VLAN_TAG_MODE))
+ return;
+
+ /*
+ * The vendor SDK forces a TPID and DEI when a ZTE rule produces an
+ * untagged service. Preserve the wire rule, but normalize the treatment
+ * consumed by the hardware-independent service resolver.
+ */
+ if (!rule->tags_to_remove && rule->treat_outer_vid >= 4096 &&
+    rule->treat_inner_vid >= 4096) {
+ rule->treat_inner_tpid_dei = 1;
+ rule->treat_inner_pbit = min_t(u8, rule->treat_inner_pbit, 8);
+ }
+}
+
+int omci_profile_resolve_multicast_ani(u8 profile,
+       u16 bridge_port_entity_id,
+       u16 *ani_entity_id)
+{
+ if (!ani_entity_id)
+ return -EINVAL;
+ if (!(omci_profile_quirks(profile) &
+      OMCI_OLT_QUIRK_DASAN_MULTICAST_ANI))
+ return -EOPNOTSUPP;
+
+ /*
+ * The legacy SDK associates a multicast GEM with the ANI allocated to
+ * its WAN MAC bridge port. Keep that stable semantic identifier in the
+ * normalized service; the hardware backend owns index allocation.
+ */
+ *ani_entity_id = bridge_port_entity_id;
+ return 0;
+}
diff --git a/net/xpon/omci/sysfs.c b/net/xpon/omci/sysfs.c
new file mode 100644
index 000000000000..577e0b53af28
--- /dev/null
+++ b/net/xpon/omci/sysfs.c
@@ -0,0 +1,697 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * OMCI sysfs interface below the owning xPON device.
+ *
+ * Generic Netlink remains the structured management API. The sysfs group
+ * exports the small, stable subset useful to shell scripts and early
userspace.
+ */
+
+#include <linux/ctype.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/kernel.h>
+#include <linux/slab.h>
+#include <linux/string.h>
+
+#include "../internal.h"
+#include "internal.h"
+
+static struct omci_device *omci_dev_from_dev(struct device *dev)
+{
+ struct xpon_device *xpon = dev_get_drvdata(dev);
+
+ return xpon ? xpon->omci : NULL;
+}
+
+static int omci_sysfs_config_set(struct omci_device *odev, u16 key,
+ const void *value, size_t len)
+{
+ u8 event = OMCI_EVENT_CONFIG_CHANGE;
+ int ret;
+
+ ret = omci_agent_config_set_userspace(odev, key, value, len,
+      OMCI_CONFIG_SOURCE_SYSFS);
+ if (ret)
+ return ret;
+
+ if (key == OMCI_CONFIG_OLT_PROFILE ||
+    key == OMCI_CONFIG_OLT_PROFILE_FORCE)
+ event = OMCI_EVENT_PROFILE_CHANGE;
+ omci_device_notify(odev, event);
+ return 0;
+}
+
+static int omci_sysfs_config_get_u8(struct omci_device *odev, u16 key,
+    u8 *value)
+{
+ size_t len = sizeof(*value);
+
+ return omci_agent_config_get(odev, key, value, &len);
+}
+
+static ssize_t dev_id_show(struct device *dev, struct device_attribute *attr,
+   char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+
+ return sysfs_emit(buf, "%u\n", odev->id);
+}
+static DEVICE_ATTR_RO(dev_id);
+
+static ssize_t ifindex_show(struct device *dev, struct device_attribute *attr,
+    char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+
+ return sysfs_emit(buf, "%u\n", odev->ifindex);
+}
+static DEVICE_ATTR_RO(ifindex);
+
+static void omci_sysfs_channel_snapshot(struct omci_device *odev, u16 *onu_id,
+ u16 *gem_port_id, u8 *state,
+ bool *channel_up)
+{
+ spin_lock_bh(&odev->state_lock);
+ if (onu_id)
+ *onu_id = odev->onu_id;
+ if (gem_port_id)
+ *gem_port_id = odev->gem_port_id;
+ if (state)
+ *state = odev->state;
+ if (channel_up)
+ *channel_up = odev->channel_up;
+ spin_unlock_bh(&odev->state_lock);
+}
+
+static ssize_t onu_id_show(struct device *dev, struct device_attribute *attr,
+   char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u16 value;
+
+ omci_sysfs_channel_snapshot(odev, &value, NULL, NULL, NULL);
+ return sysfs_emit(buf, "%u\n", value);
+}
+static DEVICE_ATTR_RO(onu_id);
+
+static ssize_t gem_port_id_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u16 value;
+
+ omci_sysfs_channel_snapshot(odev, NULL, &value, NULL, NULL);
+ return sysfs_emit(buf, "%u\n", value);
+}
+static DEVICE_ATTR_RO(gem_port_id);
+
+static ssize_t state_show(struct device *dev, struct device_attribute *attr,
+  char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u8 value;
+
+ omci_sysfs_channel_snapshot(odev, NULL, NULL, &value, NULL);
+ return sysfs_emit(buf, "%u\n", value);
+}
+static DEVICE_ATTR_RO(state);
+
+static ssize_t channel_up_show(struct device *dev,
+       struct device_attribute *attr, char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ bool value;
+
+ omci_sysfs_channel_snapshot(odev, NULL, NULL, NULL, &value);
+ return sysfs_emit(buf, "%u\n", value);
+}
+static DEVICE_ATTR_RO(channel_up);
+
+static ssize_t capabilities_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+
+ return sysfs_emit(buf, "0x%08x\n", odev->capabilities);
+}
+static DEVICE_ATTR_RO(capabilities);
+
+#define OMCI_ATOMIC64_ATTR_RO(_name, _member) \
+static ssize_t _name##_show(struct device *dev, \
+    struct device_attribute *attr, char *buf) \
+{ \
+ struct omci_device *odev = omci_dev_from_dev(dev); \
+ return sysfs_emit(buf, "%lld\n", \
+  (long long)atomic64_read(&odev->_member)); \
+} \
+static DEVICE_ATTR_RO(_name)
+
+OMCI_ATOMIC64_ATTR_RO(rx_packets, rx_packets);
+OMCI_ATOMIC64_ATTR_RO(rx_bytes, rx_bytes);
+OMCI_ATOMIC64_ATTR_RO(rx_dropped, rx_dropped);
+OMCI_ATOMIC64_ATTR_RO(tx_packets, tx_packets);
+OMCI_ATOMIC64_ATTR_RO(tx_bytes, tx_bytes);
+OMCI_ATOMIC64_ATTR_RO(tx_errors, tx_errors);
+
+#define OMCI_AGENT_ATOMIC64_ATTR_RO(_name, _member) \
+static ssize_t _name##_show(struct device *dev, \
+    struct device_attribute *attr, char *buf) \
+{ \
+ struct omci_device *odev = omci_dev_from_dev(dev); \
+ return sysfs_emit(buf, "%lld\n", \
+  (long long)atomic64_read(&odev->agent._member)); \
+} \
+static DEVICE_ATTR_RO(_name)
+
+OMCI_AGENT_ATOMIC64_ATTR_RO(agent_responses, responses);
+OMCI_AGENT_ATOMIC64_ATTR_RO(agent_duplicates, duplicates);
+OMCI_AGENT_ATOMIC64_ATTR_RO(agent_unsupported, unsupported);
+OMCI_AGENT_ATOMIC64_ATTR_RO(agent_fake_responses, fake_responses);
+
+static ssize_t mib_sync_show(struct device *dev, struct device_attribute *attr,
+     char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u16 value;
+
+ mutex_lock(&odev->agent.lock);
+ value = odev->agent.mib_sync;
+ mutex_unlock(&odev->agent.lock);
+ return sysfs_emit(buf, "%u\n", value);
+}
+static DEVICE_ATTR_RO(mib_sync);
+
+static ssize_t agent_operational_show(struct device *dev,
+      struct device_attribute *attr, char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ bool value;
+
+ mutex_lock(&odev->agent.lock);
+ value = odev->agent.operational;
+ mutex_unlock(&odev->agent.lock);
+ return sysfs_emit(buf, "%u\n", value);
+}
+static DEVICE_ATTR_RO(agent_operational);
+
+#define OMCI_CONFIG_U8_ATTR_RW(_name, _key) \
+static ssize_t _name##_show(struct device *dev, \
+    struct device_attribute *attr, char *buf) \
+{ \
+ struct omci_device *odev = omci_dev_from_dev(dev); \
+ u8 value; \
+ int ret = omci_sysfs_config_get_u8(odev, _key, &value); \
+ if (ret) \
+ return ret; \
+ return sysfs_emit(buf, "%u\n", value); \
+} \
+static ssize_t _name##_store(struct device *dev, \
+     struct device_attribute *attr, \
+     const char *buf, size_t count) \
+{ \
+ struct omci_device *odev = omci_dev_from_dev(dev); \
+ u8 value; \
+ int ret = kstrtou8(buf, 0, &value); \
+ if (ret) \
+ return ret; \
+ ret = omci_sysfs_config_set(odev, _key, &value, sizeof(value)); \
+ return ret ? ret : count; \
+} \
+static DEVICE_ATTR_RW(_name)
+
+#define OMCI_CONFIG_BOOL_ATTR_RW(_name, _key) \
+static ssize_t _name##_show(struct device *dev, \
+    struct device_attribute *attr, char *buf) \
+{ \
+ struct omci_device *odev = omci_dev_from_dev(dev); \
+ u8 value; \
+ int ret = omci_sysfs_config_get_u8(odev, _key, &value); \
+ if (ret) \
+ return ret; \
+ return sysfs_emit(buf, "%u\n", !!value); \
+} \
+static ssize_t _name##_store(struct device *dev, \
+     struct device_attribute *attr, \
+     const char *buf, size_t count) \
+{ \
+ struct omci_device *odev = omci_dev_from_dev(dev); \
+ bool enabled; \
+ u8 value; \
+ int ret = kstrtobool(buf, &enabled); \
+ if (ret) \
+ return ret; \
+ value = enabled; \
+ ret = omci_sysfs_config_set(odev, _key, &value, sizeof(value)); \
+ return ret ? ret : count; \
+} \
+static DEVICE_ATTR_RW(_name)
+
+OMCI_CONFIG_U8_ATTR_RW(traffic_management_option,
OMCI_CONFIG_TRAFFIC_MGMT_OPTION);
+OMCI_CONFIG_U8_ATTR_RW(uni_count, OMCI_CONFIG_UNI_COUNT);
+OMCI_CONFIG_U8_ATTR_RW(omcc_version, OMCI_CONFIG_OMCC_VERSION);
+OMCI_CONFIG_BOOL_ATTR_RW(agent_enabled, OMCI_CONFIG_AGENT_ENABLED);
+OMCI_CONFIG_BOOL_ATTR_RW(agent_permissive, OMCI_CONFIG_AGENT_PERMISSIVE);
+OMCI_CONFIG_BOOL_ATTR_RW(agent_fake_omci, OMCI_CONFIG_AGENT_FAKE_OMCI);
+OMCI_CONFIG_BOOL_ATTR_RW(agent_dying_gasp, OMCI_CONFIG_AGENT_DYING_GASP);
+
+static ssize_t omci_sysfs_string_show(struct omci_device *odev, u16 key,
+      char *buf)
+{
+ u8 value[OMCI_MAX_CONFIG_VALUE];
+ size_t len = sizeof(value);
+ int ret;
+
+ ret = omci_agent_config_get(odev, key, value, &len);
+ if (ret)
+ return ret;
+ return sysfs_emit(buf, "%.*s\n", (int)len, value);
+}
+
+static ssize_t omci_sysfs_string_store(struct omci_device *odev, u16 key,
+       const char *buf, size_t count)
+{
+ size_t input_count = count;
+ int ret;
+
+ while (count && (buf[count - 1] == '\n' || buf[count - 1] == '\r'))
+ count--;
+ if (!count)
+ return -EINVAL;
+
+ ret = omci_sysfs_config_set(odev, key, buf, count);
+ return ret ? ret : input_count;
+}
+
+#define OMCI_CONFIG_STRING_ATTR_RW(_name, _key) \
+static ssize_t _name##_show(struct device *dev, \
+    struct device_attribute *attr, char *buf) \
+{ \
+ return omci_sysfs_string_show(omci_dev_from_dev(dev), _key, buf); \
+} \
+static ssize_t _name##_store(struct device *dev, \
+     struct device_attribute *attr, \
+     const char *buf, size_t count) \
+{ \
+ return omci_sysfs_string_store(omci_dev_from_dev(dev), _key, buf, count); \
+} \
+static DEVICE_ATTR_RW(_name)
+
+OMCI_CONFIG_STRING_ATTR_RW(equipment_id, OMCI_CONFIG_EQUIPMENT_ID);
+OMCI_CONFIG_STRING_ATTR_RW(hardware_version, OMCI_CONFIG_HARDWARE_VERSION);
+OMCI_CONFIG_STRING_ATTR_RW(software_version_0, OMCI_CONFIG_SOFTWARE_VERSION_0);
+OMCI_CONFIG_STRING_ATTR_RW(software_version_1, OMCI_CONFIG_SOFTWARE_VERSION_1);
+
+static ssize_t vendor_id_show(struct device *dev,
+      struct device_attribute *attr, char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u8 value[4];
+ size_t len = sizeof(value);
+ unsigned int i;
+ int ret;
+
+ ret = omci_agent_config_get(odev, OMCI_CONFIG_VENDOR_ID, value, &len);
+ if (ret)
+ return ret;
+ for (i = 0; i < sizeof(value); i++)
+ if (!isprint(value[i]))
+ return sysfs_emit(buf, "%*phN\n", (int)sizeof(value), value);
+ return sysfs_emit(buf, "%.*s\n", (int)sizeof(value), value);
+}
+
+static ssize_t vendor_id_store(struct device *dev,
+       struct device_attribute *attr,
+       const char *buf, size_t count)
+{
+ return omci_sysfs_string_store(omci_dev_from_dev(dev),
+       OMCI_CONFIG_VENDOR_ID, buf, count);
+}
+static DEVICE_ATTR_RW(vendor_id);
+
+static ssize_t serial_number_show(struct device *dev,
+  struct device_attribute *attr, char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u8 value[8];
+ size_t len = sizeof(value);
+ unsigned int i;
+ int ret;
+
+ ret = omci_agent_config_get(odev, OMCI_CONFIG_SERIAL_NUMBER,
+    value, &len);
+ if (ret)
+ return ret;
+ for (i = 0; i < 4; i++)
+ if (!isprint(value[i]))
+ return sysfs_emit(buf, "%*phN\n", (int)sizeof(value), value);
+
+ return sysfs_emit(buf, "%c%c%c%c%02X%02X%02X%02X\n",
+  value[0], value[1], value[2], value[3],
+  value[4], value[5], value[6], value[7]);
+}
+
+static ssize_t serial_number_store(struct device *dev,
+   struct device_attribute *attr,
+   const char *buf, size_t count)
+{
+ return omci_sysfs_string_store(omci_dev_from_dev(dev),
+       OMCI_CONFIG_SERIAL_NUMBER, buf, count);
+}
+static DEVICE_ATTR_RW(serial_number);
+
+static ssize_t password_store(struct device *dev,
+      struct device_attribute *attr,
+      const char *buf, size_t count)
+{
+ return omci_sysfs_string_store(omci_dev_from_dev(dev), OMCI_CONFIG_PASSWORD,
+       buf, count);
+}
+static DEVICE_ATTR_WO(password);
+
+struct omci_onu_type_name {
+ u8 type;
+ const char *name;
+ const char *description;
+};
+
+static const struct omci_onu_type_name omci_onu_types[] = {
+ { OMCI_ONU_TYPE_OTHER, "Other", "Other" },
+ { OMCI_ONU_TYPE_SFU, "SFU", "Single Family Unit" },
+ { OMCI_ONU_TYPE_HGU, "HGU", "Home Gateway Unit" },
+ { OMCI_ONU_TYPE_MDU, "MDU", "Multi-Dwelling Unit" },
+ { OMCI_ONU_TYPE_SBU, "SBU", "Single Business Unit" },
+ { OMCI_ONU_TYPE_MTU, "MTU", "Multi-Tenant Unit" },
+ { OMCI_ONU_TYPE_CBU, "CBU", "Cellular Backhaul Unit" },
+};
+
+static const char *omci_sysfs_onu_type_name(u8 type)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(omci_onu_types); i++)
+ if (omci_onu_types[i].type == type)
+ return omci_onu_types[i].name;
+ return "Other";
+}
+
+static int omci_sysfs_parse_onu_type(const char *buf, size_t count, u8 *type)
+{
+ char input[32];
+ char *name;
+ unsigned int i;
+ int ret;
+
+ if (!count || count >= sizeof(input))
+ return -EINVAL;
+ memcpy(input, buf, count);
+ input[count] = '\0';
+ name = strim(input);
+
+ ret = kstrtou8(name, 0, type);
+ if (!ret)
+ return *type <= OMCI_ONU_TYPE_CBU ? 0 : -EINVAL;
+
+ for (i = 0; i < ARRAY_SIZE(omci_onu_types); i++)
+ if (!strcasecmp(name, omci_onu_types[i].name) ||
+    !strcasecmp(name, omci_onu_types[i].description)) {
+ *type = omci_onu_types[i].type;
+ return 0;
+ }
+ return -EINVAL;
+}
+
+static ssize_t onu_type_show(struct device *dev,
+     struct device_attribute *attr, char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u8 type;
+ int ret;
+
+ ret = omci_sysfs_config_get_u8(odev, OMCI_CONFIG_ONU_TYPE, &type);
+ if (ret)
+ return ret;
+ return sysfs_emit(buf, "%s\n", omci_sysfs_onu_type_name(type));
+}
+
+static ssize_t onu_type_store(struct device *dev,
+      struct device_attribute *attr,
+      const char *buf, size_t count)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u8 type;
+ int ret;
+
+ ret = omci_sysfs_parse_onu_type(buf, count, &type);
+ if (ret)
+ return ret;
+ ret = omci_sysfs_config_set(odev, OMCI_CONFIG_ONU_TYPE,
+    &type, sizeof(type));
+ return ret ? ret : count;
+}
+static DEVICE_ATTR_RW(onu_type);
+
+static ssize_t onu_type_id_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u8 type;
+ int ret;
+
+ ret = omci_sysfs_config_get_u8(odev, OMCI_CONFIG_ONU_TYPE, &type);
+ if (ret)
+ return ret;
+ return sysfs_emit(buf, "%u\n", type);
+}
+static DEVICE_ATTR_RO(onu_type_id);
+
+static int omci_sysfs_parse_profile(const char *buf, size_t count,
+    bool force, u8 *profile)
+{
+ char input[32];
+ char *name;
+ unsigned int i;
+ int ret;
+
+ if (!count || count >= sizeof(input))
+ return -EINVAL;
+ memcpy(input, buf, count);
+ input[count] = '\0';
+ name = strim(input);
+
+ ret = kstrtou8(name, 0, profile);
+ if (!ret)
+ return force ? (omci_profile_forceable(*profile) ? 0 : -EINVAL) :
+       (omci_profile_valid(*profile) ? 0 : -EINVAL);
+
+ if (force && !strcasecmp(name, "unspecified")) {
+ *profile = OMCI_OLT_PROFILE_UNSPEC;
+ return 0;
+ }
+
+ for (i = OMCI_OLT_PROFILE_GENERIC; i <= OMCI_OLT_PROFILE_ZTE; i++) {
+ if (!strcasecmp(name, omci_olt_profile_name(i))) {
+ if (force && !omci_profile_forceable(i))
+ return -EINVAL;
+ *profile = i;
+ return 0;
+ }
+ }
+ return -EINVAL;
+}
+
+static ssize_t olt_profile_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u8 profile;
+ int ret;
+
+ ret = omci_sysfs_config_get_u8(odev, OMCI_CONFIG_OLT_PROFILE, &profile);
+ if (ret)
+ return ret;
+ return sysfs_emit(buf, "%u %s\n", profile,
+  omci_olt_profile_name(profile));
+}
+
+static ssize_t olt_profile_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u8 profile;
+ int ret;
+
+ ret = omci_sysfs_parse_profile(buf, count, false, &profile);
+ if (ret)
+ return ret;
+ ret = omci_sysfs_config_set(odev, OMCI_CONFIG_OLT_PROFILE,
+    &profile, sizeof(profile));
+ return ret ? ret : count;
+}
+static DEVICE_ATTR_RW(olt_profile);
+
+static ssize_t olt_profile_force_show(struct device *dev,
+      struct device_attribute *attr, char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u8 profile;
+ int ret;
+
+ ret = omci_sysfs_config_get_u8(odev, OMCI_CONFIG_OLT_PROFILE_FORCE,
+       &profile);
+ if (ret)
+ return ret;
+ return sysfs_emit(buf, "%u %s\n", profile,
+  omci_olt_profile_name(profile));
+}
+
+static ssize_t olt_profile_force_store(struct device *dev,
+       struct device_attribute *attr,
+       const char *buf, size_t count)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u8 profile;
+ int ret;
+
+ ret = omci_sysfs_parse_profile(buf, count, true, &profile);
+ if (ret)
+ return ret;
+ ret = omci_sysfs_config_set(odev, OMCI_CONFIG_OLT_PROFILE_FORCE,
+    &profile, sizeof(profile));
+ return ret ? ret : count;
+}
+static DEVICE_ATTR_RW(olt_profile_force);
+
+static ssize_t olt_profile_effective_show(struct device *dev,
+  struct device_attribute *attr,
+  char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u8 profile;
+
+ mutex_lock(&odev->agent.lock);
+ profile = odev->agent.profile_effective;
+ mutex_unlock(&odev->agent.lock);
+ return sysfs_emit(buf, "%u %s\n", profile,
+  omci_olt_profile_name(profile));
+}
+static DEVICE_ATTR_RO(olt_profile_effective);
+
+static ssize_t olt_profile_quirks_show(struct device *dev,
+       struct device_attribute *attr, char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ u32 quirks;
+
+ mutex_lock(&odev->agent.lock);
+ quirks = odev->agent.profile_quirks;
+ mutex_unlock(&odev->agent.lock);
+ return sysfs_emit(buf, "0x%08x\n", quirks);
+}
+static DEVICE_ATTR_RO(olt_profile_quirks);
+
+static ssize_t olt_vendor_id_show(struct device *dev,
+  struct device_attribute *attr, char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ struct omci_olt_g olt = {};
+
+ if (omci_agent_olt_g_get(odev, &olt) || !olt.vendor_id_valid)
+ return sysfs_emit(buf, "unknown\n");
+ return sysfs_emit(buf, "%s\n", olt.vendor_id);
+}
+static DEVICE_ATTR_RO(olt_vendor_id);
+
+static ssize_t olt_equipment_id_show(struct device *dev,
+     struct device_attribute *attr, char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ struct omci_olt_g olt = {};
+
+ if (omci_agent_olt_g_get(odev, &olt) || !olt.equipment_id_valid)
+ return sysfs_emit(buf, "unknown\n");
+ return sysfs_emit(buf, "%s\n", olt.equipment_id);
+}
+static DEVICE_ATTR_RO(olt_equipment_id);
+
+static ssize_t olt_version_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct omci_device *odev = omci_dev_from_dev(dev);
+ struct omci_olt_g olt = {};
+
+ if (omci_agent_olt_g_get(odev, &olt) || !olt.version_valid)
+ return sysfs_emit(buf, "unknown\n");
+ return sysfs_emit(buf, "%s\n", olt.version);
+}
+static DEVICE_ATTR_RO(olt_version);
+
+static struct attribute *omci_attrs[] = {
+ &dev_attr_dev_id.attr,
+ &dev_attr_ifindex.attr,
+ &dev_attr_onu_id.attr,
+ &dev_attr_gem_port_id.attr,
+ &dev_attr_state.attr,
+ &dev_attr_channel_up.attr,
+ &dev_attr_capabilities.attr,
+ &dev_attr_rx_packets.attr,
+ &dev_attr_rx_bytes.attr,
+ &dev_attr_rx_dropped.attr,
+ &dev_attr_tx_packets.attr,
+ &dev_attr_tx_bytes.attr,
+ &dev_attr_tx_errors.attr,
+ &dev_attr_agent_responses.attr,
+ &dev_attr_agent_duplicates.attr,
+ &dev_attr_agent_unsupported.attr,
+ &dev_attr_agent_fake_responses.attr,
+ &dev_attr_mib_sync.attr,
+ &dev_attr_agent_operational.attr,
+ &dev_attr_agent_enabled.attr,
+ &dev_attr_agent_permissive.attr,
+ &dev_attr_agent_fake_omci.attr,
+ &dev_attr_agent_dying_gasp.attr,
+ &dev_attr_serial_number.attr,
+ &dev_attr_vendor_id.attr,
+ &dev_attr_password.attr,
+ &dev_attr_equipment_id.attr,
+ &dev_attr_hardware_version.attr,
+ &dev_attr_software_version_0.attr,
+ &dev_attr_software_version_1.attr,
+ &dev_attr_traffic_management_option.attr,
+ &dev_attr_onu_type.attr,
+ &dev_attr_onu_type_id.attr,
+ &dev_attr_uni_count.attr,
+ &dev_attr_omcc_version.attr,
+ &dev_attr_olt_profile.attr,
+ &dev_attr_olt_profile_force.attr,
+ &dev_attr_olt_profile_effective.attr,
+ &dev_attr_olt_profile_quirks.attr,
+ &dev_attr_olt_vendor_id.attr,
+ &dev_attr_olt_equipment_id.attr,
+ &dev_attr_olt_version.attr,
+ NULL,
+};
+static const struct attribute_group omci_group = {
+ .name = "omci",
+ .attrs = omci_attrs,
+};
+
+int omci_sysfs_init(void)
+{
+ return 0;
+}
+
+void omci_sysfs_exit(void)
+{
+}
+
+int omci_sysfs_register(struct omci_device *odev)
+{
+ return sysfs_create_group(&odev->xpon->class_dev->kobj, &omci_group);
+}
+
+void omci_sysfs_unregister(struct omci_device *odev)
+{
+ if (odev->xpon && odev->xpon->class_dev)
+ sysfs_remove_group(&odev->xpon->class_dev->kobj, &omci_group);
+}
diff --git a/net/xpon/omci/wire.c b/net/xpon/omci/wire.c
new file mode 100644
index 000000000000..d60b68587cfa
--- /dev/null
+++ b/net/xpon/omci/wire.c
@@ -0,0 +1,99 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#include <linux/errno.h>
+#include <linux/kernel.h>
+#include <linux/string.h>
+#include <linux/unaligned.h>
+
+#include "internal.h"
+#include "wire.h"
+
+int omci_wire_decode(const void *data, size_t len,
+     struct omci_wire_request *request)
+{
+ const u8 *pdu = data;
+ u16 payload_len;
+ size_t pdu_len;
+
+ if (!data || !request || len < 8)
+ return -EINVAL;
+
+ memset(request, 0, sizeof(*request));
+ request->transaction_id = get_unaligned_be16(pdu);
+ request->message_type = pdu[2];
+ request->device_id = pdu[3];
+ request->class_id = get_unaligned_be16(pdu + 4);
+ request->entity_id = get_unaligned_be16(pdu + 6);
+
+ switch (request->device_id) {
+ case OMCI_BASELINE_DEV_ID:
+ if (len != OMCI_BASELINE_LEN_NO_MIC && len != OMCI_BASELINE_LEN)
+ return -EMSGSIZE;
+ request->payload = pdu + 8;
+ request->payload_len = 32;
+ return 0;
+ case OMCI_EXTENDED_DEV_ID:
+ if (len < OMCI_EXTENDED_HEADER_LEN)
+ return -EMSGSIZE;
+ payload_len = get_unaligned_be16(pdu + 8);
+ pdu_len = OMCI_EXTENDED_HEADER_LEN + payload_len;
+ if (pdu_len > OMCI_MAX_PDU_LEN || len < pdu_len)
+ return -EMSGSIZE;
+ if (len != pdu_len && len != pdu_len + OMCI_MIC_LEN)
+ return -EMSGSIZE;
+ request->payload = pdu + OMCI_EXTENDED_HEADER_LEN;
+ request->payload_len = payload_len;
+ return 0;
+ default:
+ return -EPROTO;
+ }
+}
+
+int omci_wire_encode_response(const struct omci_wire_request *request,
+      u8 action, const void *payload,
+      size_t payload_len, void *data,
+      size_t capacity, size_t *encoded_len)
+{
+ u8 *pdu = data;
+ size_t len;
+
+ if (!request || !data || !encoded_len || (!payload && payload_len))
+ return -EINVAL;
+
+ switch (request->device_id) {
+ case OMCI_BASELINE_DEV_ID:
+ if (payload_len > 32 || capacity < OMCI_BASELINE_LEN_NO_MIC)
+ return -EMSGSIZE;
+ len = OMCI_BASELINE_LEN_NO_MIC;
+ memset(pdu, 0, len);
+ put_unaligned_be16(request->transaction_id, pdu);
+ pdu[2] = (request->message_type & 0x80) | 0x20 | action;
+ pdu[3] = OMCI_BASELINE_DEV_ID;
+ put_unaligned_be16(request->class_id, pdu + 4);
+ put_unaligned_be16(request->entity_id, pdu + 6);
+ if (payload_len)
+ memcpy(pdu + 8, payload, payload_len);
+ put_unaligned_be32(40, pdu + 40);
+ break;
+ case OMCI_EXTENDED_DEV_ID:
+ if (payload_len > U16_MAX)
+ return -EMSGSIZE;
+ len = OMCI_EXTENDED_HEADER_LEN + payload_len;
+ if (len > OMCI_MAX_PDU_LEN || capacity < len)
+ return -EMSGSIZE;
+ put_unaligned_be16(request->transaction_id, pdu);
+ pdu[2] = (request->message_type & 0x80) | 0x20 | action;
+ pdu[3] = OMCI_EXTENDED_DEV_ID;
+ put_unaligned_be16(request->class_id, pdu + 4);
+ put_unaligned_be16(request->entity_id, pdu + 6);
+ put_unaligned_be16(payload_len, pdu + 8);
+ if (payload_len)
+ memcpy(pdu + OMCI_EXTENDED_HEADER_LEN, payload,
+       payload_len);
+ break;
+ default:
+ return -EPROTO;
+ }
+
+ *encoded_len = len;
+ return 0;
+}
diff --git a/net/xpon/omci/wire.h b/net/xpon/omci/wire.h
new file mode 100644
index 000000000000..9a6b748b243e
--- /dev/null
+++ b/net/xpon/omci/wire.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _NET_OMCI_WIRE_H
+#define _NET_OMCI_WIRE_H
+
+#include <linux/types.h>
+
+struct omci_wire_request {
+ u16 transaction_id;
+ u16 class_id;
+ u16 entity_id;
+ u16 payload_len;
+ u8 message_type;
+ u8 device_id;
+ const u8 *payload;
+};
+
+int omci_wire_decode(const void *data, size_t len,
+     struct omci_wire_request *request);
+int omci_wire_encode_response(const struct omci_wire_request *request,
+      u8 action, const void *payload,
+      size_t payload_len, void *data,
+      size_t capacity, size_t *encoded_len);
+
+#endif /* _NET_OMCI_WIRE_H */
diff --git a/net/xpon/sysfs.c b/net/xpon/sysfs.c
new file mode 100644
index 000000000000..32bebc663536
--- /dev/null
+++ b/net/xpon/sysfs.c
@@ -0,0 +1,236 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <linux/device.h>
+#include <linux/netdevice.h>
+#include <linux/string.h>
+#include <net/xpon.h>
+
+#include "internal.h"
+
+struct class *xpon_class;
+
+static const char *xpon_mode_name(enum xpon_mode mode)
+{
+ switch (mode) {
+ case XPON_MODE_GPON:
+ return "gpon";
+ case XPON_MODE_EPON:
+ return "epon";
+ case XPON_MODE_XGSPON:
+ return "xgspon";
+ default:
+ return "unknown";
+ }
+}
+
+static const char *xpon_registration_name(enum xpon_registration_state state)
+{
+ switch (state) {
+ case XPON_REGISTRATION_DOWN:
+ return "down";
+ case XPON_REGISTRATION_DISCOVERY:
+ return "discovery";
+ case XPON_REGISTRATION_REGISTERING:
+ return "registering";
+ case XPON_REGISTRATION_OPERATIONAL:
+ return "operational";
+ default:
+ return "unknown";
+ }
+}
+
+static ssize_t mode_available_show(struct device *dev,
+   struct device_attribute *attr, char *buf)
+{
+ struct xpon_device *xpon = dev_get_drvdata(dev);
+ ssize_t len = 0;
+ int mode;
+
+ for (mode = 0; mode < __XPON_MODE_MAX; mode++) {
+ if (!(xpon->modes & XPON_MODE_CAP(mode)))
+ continue;
+ len += sysfs_emit_at(buf, len, "%s%s", len ? " " : "",
+     xpon_mode_name(mode));
+ }
+
+ return sysfs_emit_at(buf, len, "\n") + len;
+}
+static DEVICE_ATTR_RO(mode_available);
+
+static ssize_t mode_current_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct xpon_device *xpon = dev_get_drvdata(dev);
+
+ return sysfs_emit(buf, "%s\n", xpon_mode_name(xpon->state.mode));
+}
+
+static ssize_t mode_current_store(struct device *dev,
+  struct device_attribute *attr,
+  const char *buf, size_t count)
+{
+ struct xpon_device *xpon = dev_get_drvdata(dev);
+ enum xpon_mode mode;
+ int ret;
+
+ if (sysfs_streq(buf, "gpon"))
+ mode = XPON_MODE_GPON;
+ else if (sysfs_streq(buf, "epon"))
+ mode = XPON_MODE_EPON;
+ else if (sysfs_streq(buf, "xgspon"))
+ mode = XPON_MODE_XGSPON;
+ else
+ return -EINVAL;
+
+ ret = xpon_device_set_mode(xpon, mode);
+ return ret ? ret : count;
+}
+static DEVICE_ATTR_RW(mode_current);
+
+static ssize_t state_show(struct device *dev, struct device_attribute *attr,
+  char *buf)
+{
+ struct xpon_device *xpon = dev_get_drvdata(dev);
+
+ return sysfs_emit(buf, "%s\n",
+  xpon_registration_name(xpon->state.registration));
+}
+static DEVICE_ATTR_RO(state);
+
+static ssize_t carrier_show(struct device *dev, struct device_attribute *attr,
+    char *buf)
+{
+ struct xpon_device *xpon = dev_get_drvdata(dev);
+
+ return sysfs_emit(buf, "%u\n", xpon->state.carrier);
+}
+static DEVICE_ATTR_RO(carrier);
+
+static ssize_t signal_detect_show(struct device *dev,
+  struct device_attribute *attr, char *buf)
+{
+ struct xpon_device *xpon = dev_get_drvdata(dev);
+
+ if (!(xpon->state.valid & XPON_STATE_F_SIGNAL))
+ return sysfs_emit(buf, "unknown\n");
+ return sysfs_emit(buf, "%u\n", xpon->state.signal_detect);
+}
+static DEVICE_ATTR_RO(signal_detect);
+
+static ssize_t los_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct xpon_device *xpon = dev_get_drvdata(dev);
+
+ if (!(xpon->state.valid & XPON_STATE_F_LOS))
+ return sysfs_emit(buf, "unknown\n");
+ return sysfs_emit(buf, "%u\n", xpon->state.los);
+}
+static DEVICE_ATTR_RO(los);
+
+static struct attribute *xpon_attrs[] = {
+ &dev_attr_mode_available.attr,
+ &dev_attr_mode_current.attr,
+ &dev_attr_state.attr,
+ &dev_attr_carrier.attr,
+ &dev_attr_signal_detect.attr,
+ &dev_attr_los.attr,
+ NULL,
+};
+ATTRIBUTE_GROUPS(xpon);
+
+static int xpon_netdev_event(struct notifier_block *nb, unsigned long event,
+     void *ptr)
+{
+ struct net_device *netdev = netdev_notifier_info_to_dev(ptr);
+ struct xpon_device *xpon;
+
+ if (event != NETDEV_CHANGENAME)
+ return NOTIFY_DONE;
+
+ xpon = xpon_device_find_by_ifindex(netdev->ifindex);
+ if (!xpon)
+ return NOTIFY_DONE;
+
+ device_rename(xpon->class_dev, netdev->name);
+ xpon_device_put(xpon);
+ return NOTIFY_DONE;
+}
+
+static struct notifier_block xpon_netdev_nb = {
+ .notifier_call = xpon_netdev_event,
+};
+
+int xpon_sysfs_init(void)
+{
+ int ret;
+
+ xpon_class = class_create("xpon");
+ if (IS_ERR(xpon_class))
+ return PTR_ERR(xpon_class);
+
+ ret = register_netdevice_notifier(&xpon_netdev_nb);
+ if (ret) {
+ class_destroy(xpon_class);
+ xpon_class = NULL;
+ }
+ return ret;
+}
+
+void xpon_sysfs_exit(void)
+{
+ unregister_netdevice_notifier(&xpon_netdev_nb);
+ class_destroy(xpon_class);
+ xpon_class = NULL;
+}
+
+int xpon_sysfs_register(struct xpon_device *xpon)
+{
+ int ret;
+
+ xpon->class_dev = device_create_with_groups(xpon_class, xpon->parent,
+      MKDEV(0, 0), xpon,
+      xpon_groups, "%s",
+      xpon->netdev->name);
+ if (IS_ERR(xpon->class_dev)) {
+ ret = PTR_ERR(xpon->class_dev);
+ xpon->class_dev = NULL;
+ return ret;
+ }
+
+ if (xpon->optical) {
+ ret = sysfs_create_link(&xpon->class_dev->kobj,
+ &xpon->optical->kobj, "optical");
+ if (ret)
+ dev_warn(xpon->parent,
+ "failed to create optical xPON link: %d\n", ret);
+ }
+
+ return 0;
+}
+
+void xpon_sysfs_unregister(struct xpon_device *xpon)
+{
+ if (!xpon->class_dev)
+ return;
+ if (xpon->optical)
+ sysfs_remove_link(&xpon->class_dev->kobj, "optical");
+ device_unregister(xpon->class_dev);
+ xpon->class_dev = NULL;
+}
+
+void xpon_sysfs_notify(struct xpon_device *xpon, unsigned long changed)
+{
+ if (!xpon->class_dev)
+ return;
+ if (changed & XPON_EVENT_MODE)
+ sysfs_notify(&xpon->class_dev->kobj, NULL, "mode_current");
+ if (changed & XPON_EVENT_REGISTRATION)
+ sysfs_notify(&xpon->class_dev->kobj, NULL, "state");
+ if (changed & XPON_EVENT_CARRIER)
+ sysfs_notify(&xpon->class_dev->kobj, NULL, "carrier");
+ if (changed & XPON_EVENT_SIGNAL)
+ sysfs_notify(&xpon->class_dev->kobj, NULL, "signal_detect");
+ if (changed & XPON_EVENT_LOS)
+ sysfs_notify(&xpon->class_dev->kobj, NULL, "los");
+}

^ permalink raw reply related	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-26 21:55     ` Matheus Sampaio Queiroga
@ 2026-09-26 22:15       ` Gaoyang Wei
  2026-09-27  0:05         ` Matheus Sampaio Queiroga
  0 siblings, 1 reply; 26+ messages in thread
From: Gaoyang Wei @ 2026-09-26 22:15 UTC (permalink / raw)
  To: Matheus Sampaio Queiroga
  Cc: netdev, pbs05, Benjamin Larsson, Lorenzo Bianconi, Andrew Lunn,
	Russell King

Hi Matheus,

Thanks, this makes the reasoning much clearer.

 > with tests on en7523 and en751221 with some users, I saw them
 > attaching the OMCI network interface within a bridge, and in order
 > not to expose this interface again to a user, I implemented my OMCI
 > within the kernel

Was that OMCI interface exposed as ARPHRD_ETHER?

I ask because I do not think a packet-oriented OMCI net_device would
need Ethernet semantics at all.

For example, it could use ARPHRD_NONE, have no synthetic L2 header,
and use IFF_NOARP / IFF_POINTOPOINT.  In that case it should not be a
valid device to enslave to an Ethernet bridge in the first place.

net_device itself is not synonymous with Ethernet.  CAN is another
example of a packet-oriented protocol exposed through net_device with
different link-layer semantics.

If the previous OMCI interface used ARPHRD_ETHER, then I wonder whether
the ability to bridge it was a property of that implementation rather
than a fundamental problem with representing OMCI as a net_device.

This would still leave the separate question of whether the G.988
agent itself belongs in kernel or userspace.

 > with this I also had less overhead in the ethernet system, and faster
 > responses to OLT due to the new implementation.

This is also interesting.

Do you have any measurements for the CPU overhead and OMCI response
latency before and after moving the agent into the kernel?

It would help distinguish costs inherent to a userspace OMCI design
from costs specific to the previous implementation.

I still think it may be useful to treat OMCC transport and OMCI agent
placement as two separate design questions.

For example, a generic xPON layer might provide OMCC transport and
GEM/T-CONT operations while allowing either:

   1. an in-kernel OMCI implementation, or
   2. a userspace OMCI implementation through a packet interface.

That would avoid requiring every driver to make the same choice before
we know whether that choice is actually hardware-independent.

 > I am also aware of some Realtek SoCs having slightly different OMCI in
 > which my subsystems need to be adjusted to see this data, something
 > that fails in my current implementation

I think this is particularly useful evidence for keeping the generic
boundary small.

If Realtek requires different OMCI handling, that seems to strengthen
the case for separating common OMCC transport and PON hardware
operations from the policy of where the G.988 implementation lives.

 > The optical subsystem in which I implemented it already uses several
 > subsystems already present in Linux, so temperatures, bias and optical
 > power are already used within hwmon, for calibration I already use
 > nvmem to obtain the data and start LDDLA.

This is very close to what I had in mind.

Using hwmon for telemetry and NVMEM for calibration seems much cleaner
than exposing those through a PON-specific userspace ABI.

The remaining part I would like to understand better is the generic API
between the PON MAC and the fixed optical frontend, especially for:

   - LOS;
   - TX enable;
   - burst enable / rearm;
   - protocol and rate selection;
   - wavelength selection where applicable.

I think this is also where Benjamin's earlier BOSA/SFP discussion is
directly relevant, and I would like to hear what the PHY/phylink/SFP
maintainers think before suggesting a concrete API.

 > a good part of the subsystem depends on values coming from the
 > userspace, that's why we wrote a generic netlink and sysfs for
 > userspace communication

That also makes sense.

For ONU identity in particular, I still think it is worth separating
hardware description from provisioning policy.

Some values may come from NVMEM, some may be derived from other device
identity, and some may be supplied by userspace.  I am therefore not
sure that values such as an OMCI vendor ID should become part of the DT
ABI unless they really describe immutable hardware properties.

I will spend some time reading through the implementation you posted.

At this point I think we have two useful implementation models:

   - kernel PON MAC + userspace G.988;
   - kernel PON MAC + in-kernel G.988.

That is a good basis for identifying which parts are genuinely generic
and which parts should remain implementation choices.

Thanks,
Gaoyang

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-26 22:15       ` Gaoyang Wei
@ 2026-09-27  0:05         ` Matheus Sampaio Queiroga
  2026-09-27  4:38           ` Gaoyang Wei
  0 siblings, 1 reply; 26+ messages in thread
From: Matheus Sampaio Queiroga @ 2026-09-27  0:05 UTC (permalink / raw)
  To: Gaoyang Wei
  Cc: netdev, pbs05, Benjamin Larsson, Lorenzo Bianconi, Andrew Lunn,
	Russell King

>  > with tests on en7523 and en751221 with some users, I saw them
>  > attaching the OMCI network interface within a bridge, and in order
>  > not to expose this interface again to a user, I implemented my OMCI
>  > within the kernel
> 
> Was that OMCI interface exposed as ARPHRD_ETHER?
> 
> I ask because I do not think a packet-oriented OMCI net_device would
> need Ethernet semantics at all.
> 
> For example, it could use ARPHRD_NONE, have no synthetic L2 header,
> and use IFF_NOARP / IFF_POINTOPOINT.  In that case it should not be a
> valid device to enslave to an Ethernet bridge in the first place.
> 
> net_device itself is not synonymous with Ethernet.  CAN is another
> example of a packet-oriented protocol exposed through net_device with
> different link-layer semantics.
> 
> If the previous OMCI interface used ARPHRD_ETHER, then I wonder whether
> the ability to bridge it was a property of that implementation rather
> than a fundamental problem with representing OMCI as a net_device.
> 
> This would still leave the separate question of whether the G.988
> agent itself belongs in kernel or userspace.

I exposed it by poling in ioctl in the dirtiest way possible, by generic netlink and for sysfs. In all cases, there was an unnecessary overload on the CPU, making it very slow for other tasks
 
>  > with this I also had less overhead in the ethernet system, and faster
>  > responses to OLT due to the new implementation.
> 
> This is also interesting.
> 
> Do you have any measurements for the CPU overhead and OMCI response
> latency before and after moving the agent into the kernel?
> 
> It would help distinguish costs inherent to a userspace OMCI design
> from costs specific to the previous implementation.
> 
> I still think it may be useful to treat OMCC transport and OMCI agent
> placement as two separate design questions.
> 
> For example, a generic xPON layer might provide OMCC transport and
> GEM/T-CONT operations while allowing either:
> 
>    1. an in-kernel OMCI implementation, or
>    2. a userspace OMCI implementation through a packet interface.
> 
> That would avoid requiring every driver to make the same choice before
> we know whether that choice is actually hardware-independent.

with a similar port of the Airoha SDK I had CPU usage of around 2~15% depending on the number of MIB objects in memory, in addition to memory usage of around 122~353Mb in userspace
With OMCI in the kernel, the CPU overhead dropped to around 0.8~2.0% from what I was able to measure, and around 68~258Mb of memory usage with the same MIBs as the userspace

userspace depends on packages arriving at fe+qdma and then userspace, the omci in kernel arrives at fe+qdma and goes directly to omci without going through any other subsystem. with userspace we have responses in around 0.8~1ms, in kernel space we have responses in around 0.5~0.8ms, this for MIB queries already in memory.

for hardware queries we had responses of 1~4ms in userspace and 1ms in kernel space.

on the GEM/T-CONT after the packets are synchronized between the OLT and ONU comes the configuration of the datapath itself, on the Airoha x(gs)PON the pse/fe takes care of the GEM and T-CONT parts, what I do now is decode these values and inform the origin for the xPON subsystem, otherwise it is taken care of by hardware, besides that in the userspace I had to expose several things that are not good are exposed in userspace and much of the work ends up being in kernel space, so That's why I combined the entire system in kernel space.

>  > I am also aware of some Realtek SoCs having slightly different OMCI in
>  > which my subsystems need to be adjusted to see this data, something
>  > that fails in my current implementation
> 
> I think this is particularly useful evidence for keeping the generic
> boundary small.
> 
> If Realtek requires different OMCI handling, that seems to strengthen
> the case for separating common OMCC transport and PON hardware
> operations from the policy of where the G.988 implementation lives.

Much of what was written works with the Realtek SoC, it has the same data as the OMCI export, but instead of passing it as a skbuff package it is treated in a separate way from their systems, but contains the same data as the OMCI package.
When I said that my implementation needs to be an adapter, I'm talking about the issue of the origin of the data and the processing of the data and the delivery of the data to the hardware.
Today, a large part of the system still originates from Airoha SDK, but a lot of things don't match what was done in their SDK.

>  > The optical subsystem in which I implemented it already uses several
>  > subsystems already present in Linux, so temperatures, bias and optical
>  > power are already used within hwmon, for calibration I already use
>  > nvmem to obtain the data and start LDDLA.
> 
> This is very close to what I had in mind.
> 
> Using hwmon for telemetry and NVMEM for calibration seems much cleaner
> than exposing those through a PON-specific userspace ABI.
> 
> The remaining part I would like to understand better is the generic API
> between the PON MAC and the fixed optical frontend, especially for:
> 
>    - LOS;
>    - TX enable;
>    - burst enable / rearm;
>    - protocol and rate selection;
>    - wavelength selection where applicable.
> 
> I think this is also where Benjamin's earlier BOSA/SFP discussion is
> directly relevant, and I would like to hear what the PHY/phylink/SFP
> maintainers think before suggesting a concrete API.

Benjamin idea was to expose the LDDLA subsystem within what we currently have in "sff,sfp"/"sff,sff", but to do so we have to adapt and expose several functions and emulated data for subsystems already present. but the problem that fell was Semtech's LDDLA, since it works for several vendors,
So I implemented a more generic subsystem where it exposes several functions that the LDDLA supports, so it can provide data to the MAC xPON systems such as frequency/type that the LDDLA supports (GPON, EPON, XSPON, XEPON, etc.), BOSA status, hwmon, tx enable, rearm.

From what I've seen in LDDLA Semtech and Airoha, what controls the frequency selector is the PHY and the loaded calibration, and it's not something that the optical subsystem should deal with for now in my view, this should only happen when the PHY doesn't control this part, something that I find difficult for any vendor to implement.

>  > a good part of the subsystem depends on values coming from the
>  > userspace, that's why we wrote a generic netlink and sysfs for
>  > userspace communication
> 
> That also makes sense.
> 
> For ONU identity in particular, I still think it is worth separating
> hardware description from provisioning policy.
> 
> Some values may come from NVMEM, some may be derived from other device
> identity, and some may be supplied by userspace.  I am therefore not
> sure that values such as an OMCI vendor ID should become part of the DT
> ABI unless they really describe immutable hardware properties.
> 
> I will spend some time reading through the implementation you posted.
> 
> At this point I think we have two useful implementation models:
> 
>    - kernel PON MAC + userspace G.988;
>    - kernel PON MAC + in-kernel G.988.
> 
> That is a good basis for identifying which parts are genuinely generic
> and which parts should remain implementation choices.

values can be exchanged by the userspace controller for user-supplied values, SN, MIBs, etc...

Normally these are values predefined by each device vendor, in this example of dts it was from a TP-Link device, where all or most of them use the initials "TPLG" plus part of the device's mac address for the GPON SN, there is little data that must be fixed, the rest must come from userspace.

Then comes the issue of "profiles" where each OLT has its behavior slightly different from the ITU standard, something that could be a matter of some MIB data or even the status of the response, which is why I have some "profiles" ready in the OMCI implementation and a "Fake" status option



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-27  0:05         ` Matheus Sampaio Queiroga
@ 2026-09-27  4:38           ` Gaoyang Wei
  2026-09-27 17:47             ` Matheus Sampaio Queiroga
  0 siblings, 1 reply; 26+ messages in thread
From: Gaoyang Wei @ 2026-09-27  4:38 UTC (permalink / raw)
  To: Matheus Sampaio Queiroga
  Cc: netdev, pbs05, Benjamin Larsson, Lorenzo Bianconi, Andrew Lunn,
	Russell King

Hi Matheus,

Thanks, the measurements are useful.

I think there are two details I would like to clarify before drawing
conclusions from them.

 > I exposed it by poling in ioctl in the dirtiest way possible, by
 > generic netlink and for sysfs.

This sounds different from the packet-oriented net_device design I was
asking about.

My earlier question was specifically about the OMCI interface which
users were able to attach to a bridge: was that interface registered
as ARPHRD_ETHER?

If so, I think the bridge problem is probably orthogonal to whether
OMCI is represented by a net_device.  An ARPHRD_NONE OMCI device with
no Ethernet header should not be bridgeable in the first place.

 > with a similar port of the Airoha SDK I had CPU usage of around
 > 2~15% ...
 >
 > With OMCI in the kernel, the CPU overhead dropped to around
 > 0.8~2.0% ...

Could you describe how these numbers were measured?

In particular, it would be useful to know:

   - which SoC and CPU frequency were used;
   - whether the userspace version used polling, Generic Netlink, or a
     packet socket in the measured configuration;
   - what the MIB size / message rate was;
   - whether the memory numbers are process RSS, total system memory,
     or kernel allocations;
   - whether both implementations used the same MIB representation.

I ask because a polling ioctl/sysfs implementation would have very
different overhead from an AF_PACKET-based OMCI transport, so I do not
think we can yet attribute the measured difference to the
kernel/userspace boundary itself.

The Realtek case you described is also useful.  If the OMCI payload is
the same and only the data source and hardware delivery path differ,
that sounds like a good candidate for a small transport/backend
abstraction rather than putting those vendor differences into the
generic OMCI model.

On the optical side, your explanation also helps.  Keeping telemetry
in hwmon, calibration in NVMEM, and leaving wavelength/frequency
selection to the PHY where the PHY already owns it sounds like a
reasonable boundary to me.

Thanks,
Gaoyang

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-26 17:46 [RFC] net: towards a generic PON framework Gaoyang Wei
  2026-09-26 19:28 ` Matheus Sampaio Queiroga
@ 2026-09-27 16:57 ` Andrew Lunn
  2026-09-27 17:22   ` Gaoyang Wei
  2026-09-27 20:47 ` Benjamin Larsson
  2 siblings, 1 reply; 26+ messages in thread
From: Andrew Lunn @ 2026-09-27 16:57 UTC (permalink / raw)
  To: Gaoyang Wei
  Cc: netdev, pbs05, Matheus Sampaio Queiroga, Benjamin Larsson,
	Lorenzo Bianconi, Andrew Lunn, Russell King

On Sun, Sep 27, 2026 at 01:46:01AM +0800, Gaoyang Wei wrote:
> Hi all,
> 
> Several efforts now support, or are working towards supporting,
> Passive Optical Network (PON) hardware on Linux and OpenWrt.

I have very little knowledge about PON....

> Raw OMCI PDUs are exposed through a dedicated net_device without a
> synthetic Ethernet header and are received and transmitted by
> userspace with AF_PACKET.

I'm not making a comment on putting this signalling code in user space
vs doing it in the kernel.  But having a dedicated net_device seems
wrong to me. However what you mentioned made me think of hostapd and
wpa_supplicant. Can that sort of model be used? It does not require an
additional interface by the side of the wifi interface.

	   Andrew


^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-27 16:57 ` Andrew Lunn
@ 2026-09-27 17:22   ` Gaoyang Wei
  2026-09-27 19:24     ` Ziyou Xu
  2026-09-27 19:47     ` Andrew Lunn
  0 siblings, 2 replies; 26+ messages in thread
From: Gaoyang Wei @ 2026-09-27 17:22 UTC (permalink / raw)
  To: Andrew Lunn
  Cc: netdev, pbs05, Matheus Sampaio Queiroga, Benjamin Larsson,
	Lorenzo Bianconi, Andrew Lunn, Russell King

Hi Andrew,

Thanks, the hostapd/wpa_supplicant analogy is useful.

 > But having a dedicated net_device seems wrong to me. However what
 > you mentioned made me think of hostapd and wpa_supplicant. Can that
 > sort of model be used? It does not require an additional interface
 > by the side of the wifi interface.

Yes, I think that is worth considering.

One reason I initially found a packet net_device attractive was that
OMCI itself is a packet-oriented protocol, and an ARPHRD_NONE device
would give userspace a natural AF_PACKET interface without pretending
that OMCI is Ethernet.

But I agree that this does not necessarily mean it has to be modeled
as a separate network interface.

A model closer to nl80211 could instead keep the Ethernet service
datapath as the normal net_device and expose OMCI transport through
the xPON control interface, for example using Generic Netlink messages
associated with a PON device:

         userspace OMCI daemon
                  |
           Generic Netlink
                  |
              net/xpon
                  |
               PON MAC

with no separate omci0 device.

There is one property of the packet-interface model which I think is
important to preserve somehow, though: observability.

With a packet interface, raw OMCI traffic has a natural capture point.
It can be captured with the normal packet tooling and inspected with
Wireshark.  There are already OMCI dissectors available, for example:

   https://github.com/0liv1er/omci-wireshark-dissector

This is particularly useful for PON interoperability work.

Although G.988 defines the standard OMCI model, deployed PON networks
are not as uniform at the management-model level as the 802.11 analogy
might suggest.  Operators and vendors commonly add private MEs,
profiles, or behavioral quirks.

One concrete example is China Telecom's GPON profile.  It defines a
private "LOID authentication" ME, class 65530 (0xfffa), carrying the
operator ID, LOID, password, and authentication status.  Its behavior
also differs from an ordinary MIB object; for example it is excluded
from MIB Upload.

Cases like this make packet-level inspection very useful when an ONU
works with one OLT but fails with another.  Being able to capture the
actual OMCI exchange and feed it directly into protocol-analysis tools
can make the difference between finding an interoperability problem
quickly and debugging it indirectly through daemon logs.

So I think this also reinforces the distinction between two separate
questions:

   1. whether the G.988 agent should live in kernel or userspace;
   2. how raw OMCI PDUs should cross the kernel/userspace boundary.

Generic Netlink may well be the cleaner control abstraction, but I
would like to understand what the Linux-native equivalent of that
packet observation/debugging point should be if we do not use a
management net_device.

Do you have a particular part of the hostapd/nl80211 model in mind
for the management PDU transport itself, for example something
analogous to management frame registration/TX/RX?

And if Generic Netlink is the preferred transport, is there an
existing mechanism you would recommend for exposing the raw management
PDU stream to packet-capture/debugging tools without creating a
separate net_device?

Thanks,
Gaoyang

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-27  4:38           ` Gaoyang Wei
@ 2026-09-27 17:47             ` Matheus Sampaio Queiroga
  2026-09-27 18:27               ` Gaoyang Wei
  0 siblings, 1 reply; 26+ messages in thread
From: Matheus Sampaio Queiroga @ 2026-09-27 17:47 UTC (permalink / raw)
  To: Gaoyang Wei
  Cc: netdev, pbs05, Benjamin Larsson, Lorenzo Bianconi, Andrew Lunn,
	Russell King

>  > I exposed it by poling in ioctl in the dirtiest way possible, by
>  > generic netlink and for sysfs.
> 
> This sounds different from the packet-oriented net_device design I was
> asking about.
> 
> My earlier question was specifically about the OMCI interface which
> users were able to attach to a bridge: was that interface registered
> as ARPHRD_ETHER?
> 
> If so, I think the bridge problem is probably orthogonal to whether
> OMCI is represented by a net_device.  An ARPHRD_NONE OMCI device with
> no Ethernet header should not be bridgeable in the first place.

sdk userspace use normal netdev with this flags: NETIF_F_NO_CSUM (for old qmda versions en751221, en7528), IFF_NOARP, IFF_BROADCAST and clear IFF_MULTICAST

>  > with a similar port of the Airoha SDK I had CPU usage of around
>  > 2~15% ...
>  >
>  > With OMCI in the kernel, the CPU overhead dropped to around
>  > 0.8~2.0% ...
> 
> Could you describe how these numbers were measured?
> 
> In particular, it would be useful to know:
> 
>    - which SoC and CPU frequency were used;
>    - whether the userspace version used polling, Generic Netlink, or a
>      packet socket in the measured configuration;
>    - what the MIB size / message rate was;
>    - whether the memory numbers are process RSS, total system memory,
>      or kernel allocations;
>    - whether both implementations used the same MIB representation.
> 
> I ask because a polling ioctl/sysfs implementation would have very
> different overhead from an AF_PACKET-based OMCI transport, so I do not
> think we can yet attribute the measured difference to the
> kernel/userspace boundary itself.
> 
> The Realtek case you described is also useful.  If the OMCI payload is
> the same and only the data source and hardware delivery path differ,
> that sounds like a good candidate for a small transport/backend
> abstraction rather than putting those vendor differences into the
> generic OMCI model.
> 
> On the optical side, your explanation also helps.  Keeping telemetry
> in hwmon, calibration in NVMEM, and leaving wavelength/frequency
> selection to the PHY where the PHY already owns it sounds like a
> reasonable boundary to me.

all tests were monitoring the IRQs and threads through top and htop on SoCs of the en7523 family (en7529ct and en7529dt) with a frequency of 1Ghz and 1.2Ghz on 2CPU cores, with 256Mb and 512Mb of ram, in total 3 tests were carried out for both implementations,
with 350, 600 and 800 MIB objects in both userspace and kernel space.

The tests monitored the total use of the system with only the xPON subsystem and the basic one for a DHCP and wireless server for 15 minutes

polling tests crashed the system several times and were not fully tested

the entire system before the xpon subsystem was started was using between 60mb~80mb of ram with both userspace and kernel space



^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-27 17:47             ` Matheus Sampaio Queiroga
@ 2026-09-27 18:27               ` Gaoyang Wei
  0 siblings, 0 replies; 26+ messages in thread
From: Gaoyang Wei @ 2026-09-27 18:27 UTC (permalink / raw)
  To: Matheus Sampaio Queiroga
  Cc: netdev, pbs05, Benjamin Larsson, Lorenzo Bianconi, Andrew Lunn,
	Russell King

Hi Matheus,

Thanks, that clarifies the test setup.

The numbers are useful as an implementation comparison, although I
think they are not yet enough to attribute the difference specifically
to the kernel/userspace boundary.

In particular, the old SDK-style userspace path and the current
in-kernel implementation differ in more than just where the G.988
agent runs.  A useful follow-up experiment may be to compare the
in-kernel agent against the AN7581/AN7583 userspace implementation
using an ARPHRD_NONE packet interface and AF_PACKET, with the same MIB
size and workload.

That would help separate:

   - cost of userspace/kernel crossings;
   - cost of the transport mechanism;
   - cost of the MIB representation and implementation itself.

Regarding the old SDK netdev, your description also makes me suspect
that the bridge behavior may have come from the way that interface was
constructed as a normal Ethernet-like netdev, rather than from the
net_device abstraction itself.

Andrew has now raised the broader question of whether we need a
separate management net_device at all, so I think that is probably the
more useful interface question to pursue from here.

Thanks,
Gaoyang

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-27 17:22   ` Gaoyang Wei
@ 2026-09-27 19:24     ` Ziyou Xu
  2026-09-27 19:47     ` Andrew Lunn
  1 sibling, 0 replies; 26+ messages in thread
From: Ziyou Xu @ 2026-09-27 19:24 UTC (permalink / raw)
  To: Gaoyang Wei, Andrew Lunn
  Cc: netdev, Matheus Sampaio Queiroga, Benjamin Larsson,
	Lorenzo Bianconi, Andrew Lunn, Russell King

Hi Gaoyang,

I agree that the transport and the control API should probably be
considered separately.

I am wondering whether we need a Generic Netlink interface at all at
this stage.

If the immediate userspace requirement is only transporting raw OMCI
PDUs, AF_PACKET already provides that abstraction.  Other PON state
may remain internal to the kernel or be exposed through existing
subsystems until we have a concrete operation which requires a new
xPON-specific UAPI.

The reason I still see value in a net_device is not to model OMCI as
Ethernet, but to provide the packet endpoint required by AF_PACKET.

With an ARPHRD_NONE device, the interface could carry only raw OMCI
PDUs, without Ethernet headers, addressing, ARP, or Ethernet bridge
semantics.

There is also a practical debugging advantage to this model.

A packet endpoint gives us a natural capture point for
tcpdump/libpcap/Wireshark without having to introduce a second
debugging interface.  There are already Wireshark dissectors for
OMCI, for example:

   https://github.com/0liv1er/omci-wireshark-dissector

This is particularly useful for interoperability work.

Although G.988 specifies OMCI, deployed networks are often less
uniform than the base standard suggests.  Vendors and operators use
private managed entities, profiles, and behavioral quirks.

One example is China Telecom's LOID authentication extension, which
uses the private OMCI ME class 65530 (0xfffa).  Debugging this kind of
interoperability issue is much easier when the raw OMCI exchange can
be captured and decoded directly.

Using a packet-like netdev for OMCI is also not unique to the current
AN7581/AN7583 implementation.  There is precedent for this model in
vendor SDK/BSP implementations as well; the older Airoha SDK, for
example, also exposed OMCI through a netdev-like userspace path.

I do not think vendor precedent by itself is a reason to standardize
such an ABI, but it suggests that representing OMCI as a packet
channel maps reasonably well to existing hardware and driver designs.

Without a packet endpoint, we would need to define equivalent RX/TX,
delivery, and probably observability semantics through another
interface such as Generic Netlink.

I am not yet sure that buys us much if the only thing crossing the
kernel/userspace boundary is an opaque OMCI PDU.

So perhaps there are two questions:

   1. Is using a minimal non-Ethernet net_device purely as an
      AF_PACKET endpoint itself considered undesirable?

   2. If it is, what would be the preferred Linux-native transport for
      raw OMCI PDUs while retaining packet-level observability?

I think we should avoid defining a broader xPON Generic Netlink UAPI
until we have a concrete control operation which cannot be represented
cleanly by an existing interface.

Thanks,
Ziyou Xu

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-27 17:22   ` Gaoyang Wei
  2026-09-27 19:24     ` Ziyou Xu
@ 2026-09-27 19:47     ` Andrew Lunn
  2026-09-27 20:29       ` Ziyou Xu
  1 sibling, 1 reply; 26+ messages in thread
From: Andrew Lunn @ 2026-09-27 19:47 UTC (permalink / raw)
  To: Gaoyang Wei
  Cc: netdev, pbs05, Matheus Sampaio Queiroga, Benjamin Larsson,
	Lorenzo Bianconi, Andrew Lunn, Russell King

On Mon, Sep 28, 2026 at 01:22:13AM +0800, Gaoyang Wei wrote:
> Hi Andrew,
> 
> Thanks, the hostapd/wpa_supplicant analogy is useful.
> 
> > But having a dedicated net_device seems wrong to me. However what
> > you mentioned made me think of hostapd and wpa_supplicant. Can that
> > sort of model be used? It does not require an additional interface
> > by the side of the wifi interface.
> 
> Yes, I think that is worth considering.
> 
> One reason I initially found a packet net_device attractive was that
> OMCI itself is a packet-oriented protocol, and an ARPHRD_NONE device
> would give userspace a natural AF_PACKET interface without pretending
> that OMCI is Ethernet.

As i said, i know very little about PON, so i will likely ask lots of
dumb questions...

With hostapd/wpa_supplicant, the PDUs it needs to exchange between the
Wireless client and the access point have normal 802.11 headers.

Do raw OMCI PDUs not have headers? How can you tell apart a OMCI PDU
from a user data PDU?

Is there a PON header before the OMCI and data PDUs? Tools like pcap &
wireshark do allow for protocol specific headers, so you can have
802.3 headers, or 802.11 headers. You can even have fake headers, like
wifi RSS, and more accurate time stamps from the hardware, etc.

      Andrew

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-27 19:47     ` Andrew Lunn
@ 2026-09-27 20:29       ` Ziyou Xu
  2026-09-27 22:30         ` Andrew Lunn
  2026-09-27 22:49         ` Andrew Lunn
  0 siblings, 2 replies; 26+ messages in thread
From: Ziyou Xu @ 2026-09-27 20:29 UTC (permalink / raw)
  To: Andrew Lunn, Gaoyang Wei
  Cc: netdev, Matheus Sampaio Queiroga, Benjamin Larsson,
	Lorenzo Bianconi, Andrew Lunn, Russell King

Hi Andrew,

I think I skipped over an important layering detail in my previous mail.

An OMCI PDU does have its own protocol header, but on the wire it is 
carried inside the PON transport framing. That lower layer is also where 
the management channel is separated from normal user traffic.

A little terminology may help here.

The OLT (Optical Line Terminal) is the operator-side device, while the 
ONU (Optical Network Unit) is the subscriber-side device.

A PON is point-to-multipoint:

                          OLT
                           |
                     passive splitter
                    /       |       \
                  ONU1     ONU2     ONU3

Downstream traffic from the OLT is physically broadcast through the 
splitter to the ONUs.

Upstream transmission is shared in time. The OLT assigns transmission 
opportunities to individual ONUs, which then transmit optical bursts in 
their granted time slots.

For GPON, the transmission convergence layer is called GTC, and service 
traffic above it is carried using GEM, the GPON Encapsulation Method.

For XG-PON and XGS-PON, the corresponding layer is XGTC and service 
traffic is carried using XGEM.

OMCI is the ONU Management and Control Interface specified by ITU-T G.988.

OMCC is the ONU Management and Control Channel, the logical GEM/XGEM 
connection over which OMCI PDUs are transported.

PLOAM is another management mechanism at the TC/physical layer. It is 
used during activation for things such as ONU identification, ranging 
and assignment of transport resources. PLOAM and OMCI are separate 
protocols.

So, very approximately:

                  service plane        management plane

                   service SDU              OMCI
                        |                     |
                        |                  G.988 PDU
                        |                     |
                        +----------+----------+
                                   |
                               GEM / XGEM
                                   |
                               GTC / XGTC
                                   |
                               PON optical PHY

For GPON, a GEM frame starts with a five-octet header.

ITU-T G.984.3 defines it roughly as:

      bit
        0          11 12         23 24  26 27          39
        +-------------+-------------+------+--------------+
        |   PLI (12)  | Port-ID (12)| PTI  |   HEC (13)   |
        +-------------+-------------+------+--------------+
        |<---------------- 5 octets --------------------->|

        PLI       Payload Length Indicator
        Port-ID   GEM logical connection identifier
        PTI       Payload Type Indicator
        HEC       Header Error Control

and the frame is:

        +-----------------------+--------------------------+
        | GEM header, 5 octets  |       GEM payload       |
        +-----------------------+--------------------------+

The field relevant here is the Port-ID.

A GEM Port-ID identifies a logical connection; it is not an Ethernet 
port number. Normal service traffic is transported on service GEM 
Port-IDs, while the OMCC is transported on a GEM Port-ID assigned for 
management.

During GPON activation, the OLT configures the GEM Port-ID used by the 
OMCC through PLOAM.

A receive path can therefore look roughly like:

                     downstream GTC
                          |
                          v
                     +-----------+
                     | GEM engine|
                     +-----+-----+
                           |
                       Port-ID demux
                           |
                +----------+----------+
                |                     |
         service Port-ID         OMCC Port-ID
                |                     |
                v                     v
        service datapath          OMCI adapter
                                      |
                                      v
                                   OMCI PDU

XG-PON/XGS-PON use the same general idea with XGEM.

An XGEM header is eight octets:

      bit
        0       13 14 15 16        31 32        49 50 51        63
        +----------+-----+------------+------------+--+------------+
        | PLI (14) | Key | Port-ID(16)| Options(18)|LF|  HEC (13)  |
        +----------+-----+------------+------------+--+------------+
        |<--------------------- 8 octets -------------------------->|

Again, the Port-ID identifies the logical XGEM connection.

For XG-PON/XGS-PON, the OMCC is associated with a dedicated XGEM 
Port-ID, and the PON MAC/OMCI adapter can select and de-encapsulate 
those frames before passing the OMCI PDU further up.

So, regarding:

 > How can you tell apart a OMCI PDU from a user data PDU?

That distinction is normally made using the GEM/XGEM Port-ID before the 
OMCI implementation sees the packet.

Conceptually:

              optical stream
                   |
                GTC/XGTC
                   |
               GEM/XGEM
                   |
              Port-ID demux
                   |
              OMCC selected
                   |
         GEM/XGEM header removed
                   |
                   v
              raw OMCI PDU

The OMCI PDU itself also has its own protocol header.

For example, the beginning of a baseline OMCI message contains:

      bit
        0                    15 16       23 24       31
        +----------------------+-----------+-----------+
        | Transaction ID       | Msg type  | Device ID |
        +----------------------+-----------+-----------+
       32                    47 48                    63
        +----------------------+-----------------------+
        | ME class             | ME instance           |
        +----------------------+-----------------------+
        |                                              |
        |              message contents                |
        |                      ...                     |

The transaction identifier associates a command with its response. The 
message type identifies an OMCI operation such as Create, Delete, Set or 
Get.

The ME class and ME instance identify the Managed Entity being operated 
on. These can represent things such as the ONU itself, a T-CONT, GEM 
port, Ethernet UNI, VLAN configuration object, etc.

So when I referred to a "raw OMCI PDU", I meant something like:

         +-------------+----------------+-----+
         | OMCI header | OMCI contents  | MIC |
         +-------------+----------------+-----+

rather than:

         +-----------------+-------------+-----+
         | GEM/XGEM header | OMCI PDU    | ... |
         +-----------------+-------------+-----+

The GEM/XGEM framing has already been handled by the PON side before 
that point.

For AF_PACKET, what I had in mind was exposing this 
already-demultiplexed OMCI PDU.

Your point about protocol-specific capture headers is interesting as well.

Depending on the hardware, some information from the PON side may still 
be available after de-encapsulation. A small capture pseudo-header could 
carry things such as direction, the GEM/XGEM Port-ID, PON interface 
information, or a hardware timestamp where available.

That would be capture metadata rather than part of the OMCI PDU itself.

This also makes me wonder whether we need a Generic Netlink interface 
for the OMCI path at this stage.

If the immediate userspace requirement is simply transporting 
already-demultiplexed OMCI PDUs, AF_PACKET already provides a packet 
transport abstraction.

Without a packet endpoint, we would need to define equivalent RX/TX and 
packet-delivery semantics through another interface such as Generic 
Netlink. I am not yet sure what we would gain from doing that if the 
object crossing the boundary is simply an opaque OMCI PDU.

Other PON state could remain internal to the kernel or use existing 
subsystems until there is a concrete operation which needs an 
xPON-specific control UAPI.

So the part I am still trying to understand is whether a minimal 
non-Ethernet net_device, used only as an AF_PACKET endpoint for these 
already-decapsulated OMCI PDUs, would itself be considered the wrong 
abstraction.

If so, the hostapd/nl80211 model may indeed be a better fit, but I think 
it would be useful to understand what property of that model is 
preferable for this particular packet stream.

References:

ITU-T G.984.3
Gigabit-capable passive optical networks (G-PON):
Transmission convergence layer specification
https://www.itu.int/rec/T-REC-G.984.3

ITU-T G.987.3
10-Gigabit-capable passive optical networks (XG-PON):
Transmission convergence (TC) layer specification
https://www.itu.int/rec/T-REC-G.987.3

ITU-T G.9807.1
10-Gigabit-capable symmetric passive optical network (XGS-PON)
https://www.itu.int/rec/T-REC-G.9807.1

ITU-T G.988
ONU management and control interface (OMCI) specification
https://www.itu.int/rec/T-REC-G.988

Thanks,
Ziyou

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-26 17:46 [RFC] net: towards a generic PON framework Gaoyang Wei
  2026-09-26 19:28 ` Matheus Sampaio Queiroga
  2026-09-27 16:57 ` Andrew Lunn
@ 2026-09-27 20:47 ` Benjamin Larsson
  2026-09-27 20:58   ` Gaoyang Wei
  2 siblings, 1 reply; 26+ messages in thread
From: Benjamin Larsson @ 2026-09-27 20:47 UTC (permalink / raw)
  To: Gaoyang Wei, netdev
  Cc: pbs05, Matheus Sampaio Queiroga, Lorenzo Bianconi, Andrew Lunn,
	Russell King

Hi.

On 26/09/2026 19:46, Gaoyang Wei wrote:
> [You don't often get email from yhyxwgy@gmail.com. Learn why this is important at https://aka.ms/LearnAboutSenderIdentification ]
> 
> Hi all,
> 

[...]

Most things should reuse already existing kernel infrastructure with 
modifications where needed.

What is completely new is the omci part. And here I have come to the 
opinion that some parts (MIBs) should be handled internally by the 
kernel and some should/can only be handled by user space. Because of 
that there needs to be some kind of api where you can connect the user 
space version of the omci stack to the kernel level ploam transport 
(gpon inband side channel).

Doing it this way gives the advantage of not needing an external omci 
stack for some OLTs which is nice for development and testing purposes 
amongst other things. But interoperability amongst OLTs is really bad so 
there also needs to be a flexible way to just update the omci part in 
case you really need to amend the omci logic. (Omci is used to negotiate 
the data path).

The api also needs a way to monitor the messages sent back and forth to 
be able to triage issues. This is a common issue in pon deployments.


> 
> Questions
> =========
> 
> I would particularly appreciate feedback on the following points:
> 
> 1. Does Linux need a struct pon_device or equivalent at all, or can
>     the required concepts be represented by existing networking
>     objects?

The gpon mac data transport should expose an ethernet networking 
interface but the model for the ploam transport does not need to be one.

> 
> 2. If there is a generic PON object model, how much should it expose?
>     Are T-CONT, GEM, and LLID appropriate generic objects, or are they
>     too close to individual protocols or hardware implementations?

Does the kernel really need to handle all this? Is it not enough to just 
handle the omci interface and data pass through? At least this is the 
model we should have in an initial implementation.

> 
> 3. Should OMCI management PDUs be represented by a packet-oriented
>     interface?  If so, is a dedicated net_device appropriate?

-

> 
> 4. Should PLOAM/MPCP activation remain driver-specific, with only
>     common line state exposed by a generic layer?

Ploam is realtime and need fast handling of ploam messages, I have 
observed link failures because of debug output printing to console, 
handling it in generic code would risk exposing timing issues, it 
belongs in the hardware mac driver that then should expose the omci 
interface. That part should be common code.

> 
> 5. What should the interface between a PON MAC and fixed optical
>     frontends look like, and how much should be shared with SFP,
>     phylink, PHY, hwmon, and thermal infrastructure?
> 

Unfortunately the Econet/Airoha LDDLAs deviates from SFF-8472. So 
regarding those one option could be to create a wrapper that makes them 
look like a sfp module that you then could virtually plug into a port. 
But later on I started to change my mind and that we should model them 
what they really are. And that is as a BoB (Bi-Directional Optical 
Sub-Assembly on Board). Something very related to the sfp module but 
maybe called bob instead because that is what it is.

> 6. How should classification into GEM/LLID bearers interact with TC
>     and existing hardware-offload APIs?

As I see it we should keep everything in the ethernet domain and adapt 
the PON configuration around it, at least to start with.

> 
> 7. How much should the ITU-T PON family and IEEE EPON family share in
>     one object model?  It would be useful to avoid both
>     protocol-specific private APIs and an abstraction so generic that
>     it no longer represents the hardware usefully.

-

> 
> 8. Which configuration belongs in a networking control API
>     (rtnetlink, Generic Netlink, etc.), and which state should only be
>     observable through existing kernel subsystems?
> 

-

> Next steps
> ==========
> 
> I am intentionally not proposing a UAPI or struct definitions in this
> mail.
> 
> There is working code which can be used as a reference implementation,
> but I would prefer to get agreement on the subsystem boundary before
> turning one vendor implementation into an API which other PON drivers
> would later have to follow.
> 
> If the overall direction makes sense, the next step could be a small
> RFC patch series containing only the minimum generic model together
> with one real hardware consumer, rather than attempting to upstream a
> complete PON stack at once.
> 
> I would also be very interested in hearing from developers working on
> non-Airoha PON hardware.  A second independent hardware family would
> be particularly useful for determining whether any proposed
> abstraction is genuinely generic rather than an Airoha interface with
> generic names.
> 
I think a good way to start is with the dts and see how things end up 
and then create a RFC around that.

MvH
Benjamin Larsson

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-27 20:47 ` Benjamin Larsson
@ 2026-09-27 20:58   ` Gaoyang Wei
  0 siblings, 0 replies; 26+ messages in thread
From: Gaoyang Wei @ 2026-09-27 20:58 UTC (permalink / raw)
  To: Benjamin Larsson, netdev
  Cc: pbs05, Matheus Sampaio Queiroga, Lorenzo Bianconi, Andrew Lunn,
	Russell King

Hi Benjamin,

Thanks, this is very helpful.

 > What is completely new is the omci part. And here I have come to the
 > opinion that some parts (MIBs) should be handled internally by the
 > kernel and some should/can only be handled by user space. Because of
 > that there needs to be some kind of api where you can connect the user
 > space version of the omci stack to the kernel level ploam transport
 > (gpon inband side channel).

I think this is an important point.

The more implementations we compare, the less convinced I am that the
generic framework should decide up front where the complete G.988
agent has to live.

The AN7581/AN7583 implementation keeps the G.988 model in userspace,
while Matheus has a largely in-kernel implementation, and what you
describe sounds like a hybrid model.

That makes me think the generic boundary should probably separate:

         PON MAC / activation / transport
                     |
              OMCI transport
                     |
         +-----------+-----------+
         |                       |
    in-kernel handling      userspace OMCI
         |                       |
         +-----------+-----------+
                     |
               GEM/T-CONT /
               service setup

rather than making "OMCI is in kernel" or "OMCI is in userspace" part
of the driver model itself.

In other words, perhaps the common part should expose the OMCC
transport and the hardware operations needed by OMCI, while allowing
the actual ME/MIB logic to be split differently by different
implementations.

 > Doing it this way gives the advantage of not needing an external omci
 > stack for some OLTs which is nice for development and testing purposes
 > amongst other things. But interoperability amongst OLTs is really bad
 > so there also needs to be a flexible way to just update the omci part
 > in case you really need to amend the omci logic.

This matches what we have been seeing as well.

I think the interoperability argument is particularly important here.
Even if a kernel implementation handles the common G.988 cases, there
needs to be a way to replace or extend the higher-level OMCI behaviour
without having to modify the kernel for every OLT/vendor quirk.

That also suggests that the kernel-facing API should operate on fairly
low-level semantic objects and raw OMCI messages, rather than exposing
the full G.988 managed-entity model as a stable UAPI.

 > The api also needs a way to monitor the messages sent back and forth to
 > be able to triage issues. This is a common issue in pon deployments.

Agreed.

This is also one reason we have been looking at a packet-oriented OMCI
endpoint.  Independently of the exact API, I think observability should
be treated as a first-class requirement.

For interoperability debugging we want to be able to inspect the
actual request/response sequence, including vendor-specific MEs and
unexpected result codes, rather than only seeing the final configured
state.

Andrew has raised the question of whether a dedicated management
net_device is the right abstraction, so we are discussing that
separately.  But I think your point means that whichever transport we
choose, it should have a natural way to observe the raw OMCI exchange.

 > The gpon mac data transport should expose an ethernet networking
 > interface but the model for the ploam transport does not need to be one.

I agree with that separation.

The normal service datapath should remain an ordinary Ethernet
net_device.

PLOAM is different: it belongs to the PON MAC state machine and does
not represent an Ethernet data path.

 > Does the kernel really need to handle all this? Is it not enough to just
 > handle the omci interface and data pass through? At least this is the
 > model we should have in an initial implementation.

I think this is probably a good way to reduce the scope of the first
RFC patch series.

Rather than immediately introducing generic kernel objects for every
T-CONT, GEM port, LLID and service relationship, an initial framework
could perhaps contain only:

         - a PON device / line object;
         - line mode and activation state;
         - the normal Ethernet service datapath;
         - the OMCI transport hook;
         - the optical frontend relationship.

Then T-CONT/GEM/LLID objects would only be added once we have a real
cross-driver operation which requires them.

That would avoid turning one vendor's internal resource model into the
generic Linux model too early.

 > Ploam is realtime and need fast handling of ploam messages, I have
 > observed link failures because of debug output printing to console,
 > handling it in generic code would risk exposing timing issues, it
 > belongs in the hardware mac driver that then should expose the omci
 > interface. That part should be common code.

This is very useful implementation experience.

I agree that the timing-sensitive PLOAM FSM should remain in the MAC
driver, especially if even printk latency can disturb activation.

What I would still like the generic layer to expose is only the stable
semantic state resulting from that FSM, for example:

         O1/O2/O3/O4/O5-like activation state
         ONU-ID assigned / not assigned
         OMCC available / unavailable
         loss of signal / loss of frame

rather than trying to implement the actual timing-sensitive PLOAM
message handling in common code.

Do you think that is a reasonable split?

 > Unfortunately the Econet/Airoha LDDLAs deviates from SFF-8472. So
 > regarding those one option could be to create a wrapper that makes them
 > look like a sfp module that you then could virtually plug into a port.
 > But later on I started to change my mind and that we should model them
 > what they really are. And that is as a BoB (Bi-Directional Optical
 > Sub-Assembly on Board).

I think modelling it as what it actually is is probably cleaner as
well.

The parts which already map to existing subsystems can still use them,
for example hwmon for optical telemetry and NVMEM for calibration,
without requiring the complete device to pretend to be an SFP module.

A small BoB/optical-frontend object could then provide only the
operations which are genuinely needed by the PON MAC, such as TX
enable, burst re-arm and optical state.

That seems preferable to synthesising SFF-8472 data which does not
really exist in the hardware.

 > I think a good way to start is with the dts and see how things end up
 > and then create a RFC around that.

I would be interested in what you would put into the initial DT model.

My current thought is that DT should describe hardware topology, for
example:

         PON MAC
           |
         xPON PHY
           |
         fixed BoB / LDDLA

plus calibration/NVMEM relationships and any fixed GPIO/regulator
connections.

I would be more cautious about putting ONU identity or operator policy
into DT, since values such as serial number, LOID, OMCI vendor ID or
OLT-specific profiles may come from NVMEM, be derived, or be supplied
by userspace.

If that is also what you mean by starting from DTS, then I think it
could be a useful way to establish the hardware object boundaries
before defining any UAPI.

So at the moment I am leaning towards an intentionally small first
step:

         driver-specific PLOAM / MPCP FSM
                      |
              minimal generic PON object
                      |
           +----------+----------+
           |                     |
      Ethernet data          OMCI transport
         netdev                  |
                                 |
                      kernel and/or userspace
                          OMCI implementation

with the optical frontend represented separately and existing Linux
subsystems reused where possible.

Does that match the boundary you have in mind?

Thanks,
Gaoyang

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-27 20:29       ` Ziyou Xu
@ 2026-09-27 22:30         ` Andrew Lunn
  2026-09-27 22:49         ` Andrew Lunn
  1 sibling, 0 replies; 26+ messages in thread
From: Andrew Lunn @ 2026-09-27 22:30 UTC (permalink / raw)
  To: Ziyou Xu
  Cc: Gaoyang Wei, netdev, Matheus Sampaio Queiroga, Benjamin Larsson,
	Lorenzo Bianconi, Andrew Lunn, Russell King

> ITU-T G.984.3 defines it roughly as:
> 
>      bit
>        0          11 12         23 24  26 27          39
>        +-------------+-------------+------+--------------+
>        |   PLI (12)  | Port-ID (12)| PTI  |   HEC (13)   |
>        +-------------+-------------+------+--------------+
>        |<---------------- 5 octets --------------------->|
> 
>        PLI       Payload Length Indicator
>        Port-ID   GEM logical connection identifier
>        PTI       Payload Type Indicator
>        HEC       Header Error Control
> 
> and the frame is:
> 
>        +-----------------------+--------------------------+
>        | GEM header, 5 octets  |       GEM payload       |
>        +-----------------------+--------------------------+
> 


> The field relevant here is the Port-ID.
> > How can you tell apart a OMCI PDU from a user data PDU?
> 
> That distinction is normally made using the GEM/XGEM Port-ID before the OMCI
> implementation sees the packet.


> Depending on the hardware, some information from the PON side may still be
> available after de-encapsulation.

So in general, the hardware is doing some level of demux?

I would of put everything as it is into the skbuf, and pass it to the
network stack, telling it what the first header is. It can then look
into the header and demux it, same as it does for any other protocol
the Linux stack handles. You can also hand this off via pcap for
tcpdump/wireshark.

If the hardware is doing the demux, try to feed the skbuf in one level
higher in the stack?

> If the immediate userspace requirement is simply transporting
> already-demultiplexed OMCI PDUs, AF_PACKET already provides a packet
> transport abstraction.

Throwing out another idea, which maybe completely wrong...

An interface is just a device which receives frames from the media. It
can have multiple protocols running on top of that interface:

6: eth42: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 500
    link/none 
    inet 10.20.0.82 peer 10.20.0.1/32 scope global tun0
       valid_lft forever preferred_lft forever
    inet6 fe80::61ae:d150:2724:7d6f/64 scope link stable-privacy proto kernel_ll 
       valid_lft forever preferred_lft forever

Here there is AF_INT and AF_INT6 address families on top of the
interface.

Would it make sense to add an AF_OMCI?

> So the part I am still trying to understand is whether a minimal
> non-Ethernet net_device, used only as an AF_PACKET endpoint for these
> already-decapsulated OMCI PDUs, would itself be considered the wrong
> abstraction.

An interface which is not an interface causes a lot of confusion. The
DSA model for switches has shown that. You really want to avoid this.

    Andrew

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-27 20:29       ` Ziyou Xu
  2026-09-27 22:30         ` Andrew Lunn
@ 2026-09-27 22:49         ` Andrew Lunn
  2026-09-27 23:11           ` Gaoyang Wei
  1 sibling, 1 reply; 26+ messages in thread
From: Andrew Lunn @ 2026-09-27 22:49 UTC (permalink / raw)
  To: Ziyou Xu
  Cc: Gaoyang Wei, netdev, Matheus Sampaio Queiroga, Benjamin Larsson,
	Lorenzo Bianconi, Andrew Lunn, Russell King

On Mon, Sep 28, 2026 at 04:29:00AM +0800, Ziyou Xu wrote:
> Hi Andrew,
> 
> I think I skipped over an important layering detail in my previous mail.
> 
> An OMCI PDU does have its own protocol header, but on the wire it is carried
> inside the PON transport framing. That lower layer is also where the
> management channel is separated from normal user traffic.
> 
> A little terminology may help here.
> 
> The OLT (Optical Line Terminal) is the operator-side device, while the ONU
> (Optical Network Unit) is the subscriber-side device.
> 
> A PON is point-to-multipoint:
> 
>                          OLT
>                           |
>                     passive splitter
>                    /       |       \
>                  ONU1     ONU2     ONU3
> 
> Downstream traffic from the OLT is physically broadcast through the splitter
> to the ONUs.
> 
> Upstream transmission is shared in time. The OLT assigns transmission
> opportunities to individual ONUs, which then transmit optical bursts in
> their granted time slots.
> 
> For GPON, the transmission convergence layer is called GTC, and service
> traffic above it is carried using GEM, the GPON Encapsulation Method.
> 
> For XG-PON and XGS-PON, the corresponding layer is XGTC and service traffic
> is carried using XGEM.
> 
> OMCI is the ONU Management and Control Interface specified by ITU-T G.988.
> 
> OMCC is the ONU Management and Control Channel, the logical GEM/XGEM
> connection over which OMCI PDUs are transported.
> 
> PLOAM is another management mechanism at the TC/physical layer. It is used
> during activation for things such as ONU identification, ranging and
> assignment of transport resources. PLOAM and OMCI are separate protocols.
> 
> So, very approximately:
> 
>                  service plane        management plane
> 
>                   service SDU              OMCI
>                        |                     |
>                        |                  G.988 PDU
>                        |                     |
>                        +----------+----------+
>                                   |
>                               GEM / XGEM
>                                   |
>                               GTC / XGTC
>                                   |
>                               PON optical PHY
> 
> For GPON, a GEM frame starts with a five-octet header.
> 
> ITU-T G.984.3 defines it roughly as:
> 
>      bit
>        0          11 12         23 24  26 27          39
>        +-------------+-------------+------+--------------+
>        |   PLI (12)  | Port-ID (12)| PTI  |   HEC (13)   |
>        +-------------+-------------+------+--------------+
>        |<---------------- 5 octets --------------------->|
> 
>        PLI       Payload Length Indicator
>        Port-ID   GEM logical connection identifier
>        PTI       Payload Type Indicator
>        HEC       Header Error Control
> 
> and the frame is:
> 
>        +-----------------------+--------------------------+
>        | GEM header, 5 octets  |       GEM payload       |
>        +-----------------------+--------------------------+
> 
> The field relevant here is the Port-ID.
> 
> A GEM Port-ID identifies a logical connection; it is not an Ethernet port
> number. Normal service traffic is transported on service GEM Port-IDs, while
> the OMCC is transported on a GEM Port-ID assigned for management.
 
Another question which might affect the architecture. From what you
have described, an ONU might be assigned multiple Port-IDs so it can
carry multiple, separated, service traffic streams? You would want to
represent each stream as a Linux netdev, with its own IP addresses
etc.

This again made me think of 802.11 in Linux. I'm not too familiar with
it, but it has the concept of a wiphy, which represents the lowest
levels of the hardware. You can then instantiate clients & access
points on top of it, each being a linux netdev. They are share the
same lower level, all need to use the same frequency, but you can have
multiple clients connected to multiple different AP, or an interface
offering multiple access points with different SSID, or both a client
and an access point, which when combined would make a WiFi extender
etc.

You might need something similar, an entity which represents the lower
levels of the ONU, and then you instantiate netdevs on top of it as
dictated by management.

     Andrew


^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-27 22:49         ` Andrew Lunn
@ 2026-09-27 23:11           ` Gaoyang Wei
  2026-09-28  1:25             ` Andrew Lunn
  0 siblings, 1 reply; 26+ messages in thread
From: Gaoyang Wei @ 2026-09-27 23:11 UTC (permalink / raw)
  To: Andrew Lunn, Ziyou Xu
  Cc: netdev, Matheus Sampaio Queiroga, Benjamin Larsson,
	Lorenzo Bianconi, Andrew Lunn, Russell King

Hi Andrew,

 > Another question which might affect the architecture. From what you
 > have described, an ONU might be assigned multiple Port-IDs so it can
 > carry multiple, separated, service traffic streams? You would want to
 > represent each stream as a Linux netdev, with its own IP addresses
 > etc.

An ONU can indeed have multiple GEM Port-IDs, but I think there is one
important distinction here: a GEM Port-ID is not necessarily equivalent
to one Linux service interface.

G.988 has an interworking layer between the GEM connections and the
Ethernet-facing service.

For example, one Ethernet UNI can use an IEEE 802.1p mapper which sends
different priority classes to different GEM ports.  Bridge and VLAN
configuration can also make the relationship between Ethernet services
and GEM connections non-1:1.

Very roughly:

                       Ethernet UNI
                            |
                    bridge / mapper /
                      VLAN handling
                      /     |     \
                     /      |      \
               GEM port  GEM port  GEM port
                  100       101       102

So I think GEM Port-IDs are better viewed as PON bearer resources.

A Linux netdev should represent an actual service interface where that
is meaningful, rather than automatically creating one netdev for every
GEM Port-ID.

This is also why I was hesitant in the original RFC to make GEM ports
the central Linux object.  They are important hardware resources, but
the Linux service topology above them can be different.

 > This again made me think of 802.11 in Linux. I'm not too familiar with
 > it, but it has the concept of a wiphy, which represents the lowest
 > levels of the hardware. You can then instantiate clients & access
 > points on top of it, each being a linux netdev.
 >
 > [...]
 >
 > You might need something similar, an entity which represents the lower
 > levels of the ONU, and then you instantiate netdevs on top of it as
 > dictated by management.

I think this may be the missing abstraction.

It also fits very well with your AF_OMCI suggestion in the other mail.

When I originally thought about a packet-oriented OMCI interface,
SocketCAN was one of the models I had in mind: a non-Ethernet protocol
using the Linux networking stack and normal socket/packet tooling.

But your AF_OMCI suggestion makes me think I may have borrowed the
wrong part of that model.

With SocketCAN, can0 represents a real CAN interface, and PF_CAN
sockets operate on that interface:

         physical CAN controller
                  |
                can0
                  |
           PF_CAN socket binds
              to can0

The CAN protocol itself does not require creating another synthetic
interface next to can0.

Ziyou's current OMCI implementation effectively looks more like:

                   PON hardware
                   /          \
            Ethernet path     omci0
                                 |
                             AF_PACKET

where omci0 exists mainly to provide a packet endpoint for the
already-demultiplexed OMCI PDUs.

Putting your two suggestions together seems to lead to a cleaner model:

                      +------------------+
                      |    PON device    |
                      |  (wiphy-like)    |
                      +--------+---------+
                               |
                 +-------------+-------------+
                 |                           |
              AF_OMCI                 service interfaces
                 |                           |
           OMCI userspace             Linux net_device(s)
                                             |
                                    VLAN / TC / bridge
                                             |
                                       GEM mapping

The PON object would represent the common lower level: the PON MAC,
line/activation state, and its relationship with the optical frontend.

The service netdevs above it would represent actual Linux-visible
Ethernet services where needed.

The GEM/T-CONT resources would remain below that service abstraction.
They could still be managed or offloaded by the driver, but there
would not necessarily be one netdev per GEM Port-ID.

This also gives a much stronger reason for one of the original RFC
questions about whether Linux needs a struct pon_device or equivalent.

Initially I was thinking such an object might mainly be useful as a
container for PON-specific state and resources such as GEM ports and
T-CONTs.

A wiphy-like model suggests a simpler and more fundamental purpose:
represent the shared PON line/hardware itself.

The minimum object might therefore only need to represent things such
as:

         PON mode
         activation / line state
         associated PON MAC / driver
         associated optical frontend
         service-interface relationships
         management-protocol attachment points

without making the complete GEM/T-CONT/service graph part of the first
generic API.

That also seems to answer part of the question raised by your AF_OMCI
idea.

If OMCI is a protocol associated with this lower-level PON object,
then there is no need for a synthetic omci0 merely to obtain
AF_PACKET semantics.

Conceptually the receive path becomes:

              PON MAC
                 |
          GEM/XGEM demux
           /           \
    service bearer      OMCC
         |               |
  service mapping     OMCI PDU
         |               |
      netdev(s)        AF_OMCI

and TX follows the reverse path.

I would still avoid defining the AF_OMCI socket ABI at this point,
because one question remains: unlike can0, a wiphy itself is not a
net_device.

So if the PON parent is similarly a non-netdev object, AF_OMCI would
need some way to identify the PON object rather than simply binding to
a service-netdev ifindex.

I do not think that is a reason to create another synthetic net_device
just to obtain an ifindex, though.  It seems better to first decide
what represents the PON device, and then design the management
protocol attachment around that object.

Is that approximately what you had in mind with the wiphy analogy?

I think packet capture should be kept separate from this question as
well.  Good observability of OMCI traffic is still important for PON
interoperability debugging, but that does not require the OMCI
transport endpoint itself to be a net_device.

Thanks,
Gaoyang

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-27 23:11           ` Gaoyang Wei
@ 2026-09-28  1:25             ` Andrew Lunn
  2026-09-28  8:37               ` John Crispin
  0 siblings, 1 reply; 26+ messages in thread
From: Andrew Lunn @ 2026-09-28  1:25 UTC (permalink / raw)
  To: Gaoyang Wei
  Cc: Ziyou Xu, netdev, Matheus Sampaio Queiroga, Benjamin Larsson,
	Lorenzo Bianconi, Andrew Lunn, Russell King

On Mon, Sep 28, 2026 at 07:11:19AM +0800, Gaoyang Wei wrote:
> Hi Andrew,
> 
> > Another question which might affect the architecture. From what you
> > have described, an ONU might be assigned multiple Port-IDs so it can
> > carry multiple, separated, service traffic streams? You would want to
> > represent each stream as a Linux netdev, with its own IP addresses
> > etc.
> 
> An ONU can indeed have multiple GEM Port-IDs, but I think there is one
> important distinction here: a GEM Port-ID is not necessarily equivalent
> to one Linux service interface.
> 
> G.988 has an interworking layer between the GEM connections and the
> Ethernet-facing service.
> 
> For example, one Ethernet UNI can use an IEEE 802.1p mapper which sends
> different priority classes to different GEM ports.  Bridge and VLAN
> configuration can also make the relationship between Ethernet services
> and GEM connections non-1:1.
> 
> Very roughly:
> 
>                       Ethernet UNI
>                            |
>                    bridge / mapper /
>                      VLAN handling
>                      /     |     \
>                     /      |      \
>               GEM port  GEM port  GEM port
>                  100       101       102

I think that is too roughly, because that is not how Linux actually
works.

A normal setup would be

                 br0
                  | 
		Bridge
		|    |
               eth0 eth1

and

	vlan0:42
	 |
	 eth0

What it sounds like it should be is


                 br0
                  | 
		Bridge
		|    |
               eth0 eth1
                |    
       ------mapper------
      /        |         \ 
    GEM port  GEM port  GEM port
       100       101       102

You get your 802.1p tagged frames flowing down from user space and at
the bottom you map them to GEM ports.

How you classify frames above to tag them for 802.1p should be out of
scope at this level, it is a TC problem, or done via the VLAN
interfaces stacked on top of the base interface. It is a solved
problem Linux can already do. It is the mapper which is new.

But i assume this is also possible:


               eth0                                 eth1		
                |    		                     |    		
       ------mapper------	            ------mapper------		
      /        |         \ 	           /        |         \ 	
    GEM port  GEM port  GEM port         GEM port  GEM port  GEM port
       100       101       102              103       104       105  


> Putting your two suggestions together seems to lead to a cleaner model:
> 
>                      +------------------+
>                      |    PON device    |
>                      |  (wiphy-like)    |
>                      +--------+---------+
>                               |
>                 +-------------+-------------+
>                 |                           |
>              AF_OMCI                 service interfaces
>                 |                           |
>           OMCI userspace             Linux net_device(s)
>                                             |
>                                    VLAN / TC / bridge
>                                             |
>                                       GEM mapping

You might want to talk to the wifi developers. Do they actually like
the wiphy model, does it work for them, or do they wish they could
throw it away and start again? What would they do differently?

> That also seems to answer part of the question raised by your AF_OMCI
> idea.
> 
> If OMCI is a protocol associated with this lower-level PON object,
> then there is no need for a synthetic omci0 merely to obtain
> AF_PACKET semantics.

So as you say, one question is, how do you associate an AF_OMCI socket
to the lower level PON object? A typically home router probably has a
single SFP cage, but maybe somebody will build a box with two SFP
cages, you can connect to two independent providers so you have
redundancy, and load balancing, etc.

How does addressing work at this level? BSD Sockets can use bind(1) to
set the local address. Or you have SO_BINDTODEVICE socket option, if
there is a name which can be used. I don't think it needs an ifindex.

	Andrew


^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-28  1:25             ` Andrew Lunn
@ 2026-09-28  8:37               ` John Crispin
  2026-09-28 10:54                 ` Benjamin Larsson
                                   ` (2 more replies)
  0 siblings, 3 replies; 26+ messages in thread
From: John Crispin @ 2026-09-28  8:37 UTC (permalink / raw)
  To: Andrew Lunn
  Cc: Ziyou Xu, netdev, Matheus Sampaio Queiroga, Benjamin Larsson,
	Lorenzo Bianconi, Andrew Lunn, Russell King, Gaoyang Wei

Hi all,

Christian Marangi has been working on the design of a PON subsystem
together with Airoha for almost a year. I started implementing it on
the AN7581 two months ago. Reading this thread was a bit of a
surprise, because the design you are converging on is very close to
what we already have running and were planning to propose upstream in
the near future. So rather than add another design on paper, let me
describe working code.

The design is modelled on nl80211/cfg80211 with a fullmac driver.

net/pon has a struct pon_dev for each PON MAC. It is not a netdev,
which is what Andrew described with the wiphy comparison. Userspace
talks to it by a device id, not an ifindex.

The MAC driver runs PLOAM and the key hierarchy itself, as Benjamin
suggests. It reports the activation state to the core. The core checks
it, publishes it together with the alarms and sets carrier from it.

Service traffic uses normal netdevs. There is one data netdev, plus
GEM netdevs where a GEM port needs its own bridge port. Frames leave
through a GDM port of the Ethernet driver, which acts as a conduit in
the DSA sense.

Andrew's mapper exists too. A classifier on the data netdev maps
VLAN and 802.1p to GEM ports. The core keeps the T-CONTs, the GEM
ports and these rules. It replays them after a link loss. Everything
above that is plain bridge, VLAN devices and tc. T-CONT scheduling is
an offloaded ets qdisc.

The design reuses existing infrastructure wherever it can. The optics
report through hwmon and take their calibration from NVMEM. The PON
serdes is a generic PHY. The PHY core only needed a PHY_MODE_PON and a
set of PON configure options. Sync status, FEC counters and three
reports (ready, loss, TX fault) go through a small PON side channel.
That could become part of the PHY core, but that is a problem for
another day.

Control is a single generic netlink family with a YAML spec. OMCI runs
in userspace, in a GPL daemon that sets up the PON objects over that
family and everything Ethernet over bridge, tc, ethtool and rtnetlink.

There is also a small command line tool, pon-tool, which plays the
role iw plays for nl80211. It has a verb for every attribute of the
family, so the whole subsystem can be inspected and driven by hand.
It dumps the device, T-CONTs, GEM ports, classifier rules, counters
and alarms. A monitor mode follows the PLOAM state, alarms and object
changes live. GEM netdevs are created over rtnetlink with their own
link kind, the same way iw creates wireless interfaces.

The current code reaches O5, passes traffic and survives fibre pulls
against a production OLT. I have successfully tested HGU, SFU and IP
host services running concurrently. PON flows are also offloaded in
hardware through the Airoha PPE and NPU.

On the OMCI transport I think Andrew is right that omci0 has to go.
Today it is an ARPHRD_NONE conduit used with AF_PACKET, which is an
interface that is not really an interface. Rather than add a new
address family, I would follow what hostapd and wpa_supplicant do.
Wifi once relied on a monitor interface for management frames. It
moved to frame registration, TX and RX over generic netlink. The PON
family would get the same: a daemon registers for OMCI on a PON
device and receives and sends PDUs as netlink messages. That keeps
the subsystem on the design that already works for wifi and needs no
new address family. The core already knows which GEM port carries
the OMCC, so there is nothing to classify.

Benjamin, you wrote that some parts of the MIB should be handled
inside the kernel. Which MEs do you have in mind? The OMCC transport
belongs to the core, so an in-kernel handler could attach to it. I
would like to understand which MEs need it before we design for it.

If all of this sounds plausible, I can post an RFC series of the
subsystem within a week, together with a link to a git repository
with pon-tool. Before that I want to move the OMCI transport to
generic netlink and validate it on a board.

	John


On 28.09.26 03:25, Andrew Lunn wrote:
> On Mon, Sep 28, 2026 at 07:11:19AM +0800, Gaoyang Wei wrote:
>> Hi Andrew,
>>
>>> Another question which might affect the architecture. From what you
>>> have described, an ONU might be assigned multiple Port-IDs so it can
>>> carry multiple, separated, service traffic streams? You would want to
>>> represent each stream as a Linux netdev, with its own IP addresses
>>> etc.
>>
>> An ONU can indeed have multiple GEM Port-IDs, but I think there is one
>> important distinction here: a GEM Port-ID is not necessarily equivalent
>> to one Linux service interface.
>>
>> G.988 has an interworking layer between the GEM connections and the
>> Ethernet-facing service.
>>
>> For example, one Ethernet UNI can use an IEEE 802.1p mapper which sends
>> different priority classes to different GEM ports.  Bridge and VLAN
>> configuration can also make the relationship between Ethernet services
>> and GEM connections non-1:1.
>>
>> Very roughly:
>>
>>                       Ethernet UNI
>>                            |
>>                    bridge / mapper /
>>                      VLAN handling
>>                      /     |     \
>>                     /      |      \
>>               GEM port  GEM port  GEM port
>>                  100       101       102
> 
> I think that is too roughly, because that is not how Linux actually
> works.
> 
> A normal setup would be
> 
>                  br0
>                   | 
> 		Bridge
> 		|    |
>                eth0 eth1
> 
> and
> 
> 	vlan0:42
> 	 |
> 	 eth0
> 
> What it sounds like it should be is
> 
> 
>                  br0
>                   | 
> 		Bridge
> 		|    |
>                eth0 eth1
>                 |    
>        ------mapper------
>       /        |         \ 
>     GEM port  GEM port  GEM port
>        100       101       102
> 
> You get your 802.1p tagged frames flowing down from user space and at
> the bottom you map them to GEM ports.
> 
> How you classify frames above to tag them for 802.1p should be out of
> scope at this level, it is a TC problem, or done via the VLAN
> interfaces stacked on top of the base interface. It is a solved
> problem Linux can already do. It is the mapper which is new.
> 
> But i assume this is also possible:
> 
> 
>                eth0                                 eth1		
>                 |    		                     |    		
>        ------mapper------	            ------mapper------		
>       /        |         \ 	           /        |         \ 	
>     GEM port  GEM port  GEM port         GEM port  GEM port  GEM port
>        100       101       102              103       104       105  
> 
> 
>> Putting your two suggestions together seems to lead to a cleaner model:
>>
>>                      +------------------+
>>                      |    PON device    |
>>                      |  (wiphy-like)    |
>>                      +--------+---------+
>>                               |
>>                 +-------------+-------------+
>>                 |                           |
>>              AF_OMCI                 service interfaces
>>                 |                           |
>>           OMCI userspace             Linux net_device(s)
>>                                             |
>>                                    VLAN / TC / bridge
>>                                             |
>>                                       GEM mapping
> 
> You might want to talk to the wifi developers. Do they actually like
> the wiphy model, does it work for them, or do they wish they could
> throw it away and start again? What would they do differently?
> 
>> That also seems to answer part of the question raised by your AF_OMCI
>> idea.
>>
>> If OMCI is a protocol associated with this lower-level PON object,
>> then there is no need for a synthetic omci0 merely to obtain
>> AF_PACKET semantics.
> 
> So as you say, one question is, how do you associate an AF_OMCI socket
> to the lower level PON object? A typically home router probably has a
> single SFP cage, but maybe somebody will build a box with two SFP
> cages, you can connect to two independent providers so you have
> redundancy, and load balancing, etc.
> 
> How does addressing work at this level? BSD Sockets can use bind(1) to
> set the local address. Or you have SO_BINDTODEVICE socket option, if
> there is a name which can be used. I don't think it needs an ifindex.
> 
> 	Andrew
> 
> 


^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-28  8:37               ` John Crispin
@ 2026-09-28 10:54                 ` Benjamin Larsson
  2026-09-28 14:10                   ` Andrew Lunn
  2026-09-28 11:23                 ` Gaoyang Wei
  2026-09-28 14:05                 ` Andrew Lunn
  2 siblings, 1 reply; 26+ messages in thread
From: Benjamin Larsson @ 2026-09-28 10:54 UTC (permalink / raw)
  To: John Crispin, Andrew Lunn
  Cc: Ziyou Xu, netdev, Matheus Sampaio Queiroga, Lorenzo Bianconi,
	Andrew Lunn, Russell King, Gaoyang Wei

Hi.

On 9/28/26 10:37, John Crispin wrote:
> 
> Hi all,
> 

[...]

> 
> Benjamin, you wrote that some parts of the MIB should be handled
> inside the kernel. Which MEs do you have in mind? The OMCC transport
> belongs to the core, so an in-kernel handler could attach to it. I
> would like to understand which MEs need it before we design for it.
> 
> If all of this sounds plausible, I can post an RFC series of the
> subsystem within a week, together with a link to a git repository
> with pon-tool. Before that I want to move the OMCI transport to
> generic netlink and validate it on a board.
> 
>          John
> 
The opinion behind this is that if the requested information is from the 
kernel then it could/should be directly handled by the kernel. And in 
the case when the OLT is just acting like a dummy bridge it would be 
convenient to have a bare minimum kernel omci stack so a user space one 
would not be needed.

If we look into actual MEs then 131 (ToD, 1588) probably needs to be 
handled in kernel. Then we have counters for a lot of different things 
where the source is often the pon mac (ME 312, 341 ect). And then we 
have ANI-G 263 (optical statistics).

In the case of ANI-G 263 I looked at hwmon a while ago and IIRC there 
was no nice way to consume hwmon information from inside the kernel, I 
am quite sure this will be the case for a lot of other MEs.

So a hybrid omci stack where you can override specific MEs if you want 
could be a solution, a somewhat complex solution.

But if we go back to what really needs to be in the kernel the answer is 
nothing as every vendor I know of has a user space omci daemon.

Anyway great that there is work happening in this area.

MvH
Benjamin Larsson

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-28  8:37               ` John Crispin
  2026-09-28 10:54                 ` Benjamin Larsson
@ 2026-09-28 11:23                 ` Gaoyang Wei
  2026-09-28 14:05                 ` Andrew Lunn
  2 siblings, 0 replies; 26+ messages in thread
From: Gaoyang Wei @ 2026-09-28 11:23 UTC (permalink / raw)
  To: John Crispin, Andrew Lunn
  Cc: Ziyou Xu, netdev, Matheus Sampaio Queiroga, Benjamin Larsson,
	Lorenzo Bianconi, Andrew Lunn, Russell King, Christian Marangi,
	upstream

Hi John,

Thanks, this is very useful, and Benjamin's follow-up also helps narrow
the problem.

Having working code changes the discussion considerably, but I would
still like to be careful not to make the boundaries of one working
Airoha implementation become the generic Linux model before we compare
them against another implementation.

Ziyou and I have started looking more closely at public PON software
from other vendors for exactly that reason.

The most useful non-Airoha prior art I have found so far is the
MaxLinear/Intel/Lantiq PON software:

   https://github.com/maxlinear/pon_net_lib

This is not the PON MAC/PHY driver itself.  It is a userspace
network-adaptation layer between the OMCI/service model and Linux
networking.

Its ChangeLog is interesting because it shows many of the same
boundaries we are discussing here.

For example, it contains support for GEM/T-CONT relationships,
VEIP/PPTP Ethernet UNI, Extended VLAN, hierarchical qdisc/QoS and
multicast, but implements much of the networking side using Linux
netlink, TC, qdisc and bridge mechanisms.

There are also some particularly relevant historical changes:

   - OMCI packets were classified and trapped to the CPU using a TC
     trap action;

   - functionality from a private pon_mcc_drv was moved towards
     standard kernel interfaces;

   - the old "OMCI Bridge" was removed;

   - their VLAN handling was changed when the corresponding VLAN action
     interface became available upstream.

The MaxLinear platform is not just an abandoned historical codebase,
either.  Their current prplOS profiles still build PON WAN images:

   https://github.com/maxlinear/mxl_prplos_profiles

So this looks like useful independent evidence that at least the
general direction

         userspace management model
                 |
         standard Linux networking
                 |
         PON-specific bearer mapping

is not unique to the Airoha architecture.

Realtek may become another useful comparison point, although it is not
ready yet as a clean reference implementation.

There is an active OpenWrt RTL9607C/RTL8198D bring-up:

   https://github.com/openwrt/openwrt/pull/20064

and separate community work has reported working GPON on RTL9602C and
GPON/Ethernet/HW offload on RTL9607F:

   https://github.com/Anime4000/RTL960x/discussions/474

However, the PON/OMCI side still has proprietary components and a lot
of reverse-engineering work, so I would not use it to define an ABI
today.

Broadcom BCM6858 is even less useful for this purpose at the moment:
the SoC/platform support is public, but I have not found a comparable
public PON MAC/PHY/OMCI implementation.

This makes your implementation particularly valuable, but it also
makes me want to distinguish two questions:

         does this model work well for Airoha?

and

         which parts of this model are actually generic PON concepts?

 > net/pon has a struct pon_dev for each PON MAC. It is not a netdev,
 > which is what Andrew described with the wiphy comparison. Userspace
 > talks to it by a device id, not an ifindex.

This looks like a good answer to the lower-level object question we
had just reached.

I would especially like to review:

         - pon_dev lifetime and ownership;
         - device-id allocation and namespace semantics;
         - relationships between pon_dev and service netdevs;
         - how multiple PON MACs are represented;
         - which state is considered generic versus driver-private.

I agree with Andrew that this is probably better than creating a
synthetic net_device merely to obtain an ifindex.

 > Andrew's mapper exists too. A classifier on the data netdev maps
 > VLAN and 802.1p to GEM ports. The core keeps the T-CONTs, the GEM
 > ports and these rules. It replays them after a link loss. Everything
 > above that is plain bridge, VLAN devices and tc. T-CONT scheduling is
 > an offloaded ets qdisc.

This is probably the part I would most like to compare against another
vendor.

I agree with the upper boundary:

         bridge / VLAN / TC
                |
           classified traffic
                |
              mapper
                |
           PON bearer

The new PON-specific operation is the mapping from Linux networking
state to a bearer; Linux does not need another PON-specific VLAN or
packet classifier.

What I am less sure about yet is ownership below that boundary.

In particular, why does the generic PON core need to own and replay
the T-CONTs, GEM ports and classifier rules, rather than having some
of that state owned by the driver or by the networking/offload objects
which created it?

Your hardware may require explicit replay after loss of activation,
while another implementation may retain the state in firmware, rebuild
it from userspace, or expose a different lifetime entirely.

This is exactly the kind of detail where a second hardware family
would help us determine whether the object belongs in net/pon or only
in the Airoha driver.

The old MaxLinear code may also be useful here as prior art.  Its
history contains both GEM/T-CONT objects and Linux TC/qdisc/netdev
integration, so comparing the lifetime and ownership model may expose
which assumptions are hardware-specific.

 > Service traffic uses normal netdevs. There is one data netdev, plus
 > GEM netdevs where a GEM port needs its own bridge port.

I like that this does not imply one netdev per GEM Port-ID.

I would like to understand the exact criterion for when a GEM becomes
a netdev, though.

If "needs to be a bridge port" is the criterion, then it would be
useful to document what Linux semantics that GEM netdev represents,
and whether the same service can also be represented purely through
the mapper without creating such a netdev.

 > Control is a single generic netlink family with a YAML spec. OMCI runs
 > in userspace, in a GPL daemon that sets up the PON objects over that
 > family and everything Ethernet over bridge, tc, ethtool and rtnetlink.

This is probably the part where seeing the YAML specification early is
most important.

One of the main reasons for this RFC was to avoid freezing a
vendor-specific interface and only later discovering that another PON
MAC cannot implement the same model cleanly.

So I would like to review the Generic Netlink schema together with the
object lifetimes rather than treat it as an already-established ABI.

Benjamin's follow-up also seems to support keeping the first version
small here.

There are MEs whose underlying data naturally comes from the kernel,
such as hardware counters, timestamps or optical telemetry, but that
does not necessarily mean that the G.988 ME itself needs to live in
the kernel.

The kernel can expose the primitive state through the appropriate
Linux interface and the userspace OMCI agent can translate that into
G.988 semantics.

That seems preferable to introducing an in-kernel ME framework until
we have a concrete ME which cannot reasonably be implemented that way.

 > On the OMCI transport I think Andrew is right that omci0 has to go.
 > Today it is an ARPHRD_NONE conduit used with AF_PACKET, which is an
 > interface that is not really an interface. Rather than add a new
 > address family, I would follow what hostapd and wpa_supplicant do.

I agree that this is now the most promising direction.

The AF_PACKET design was attractive mainly because it gave us packet
semantics and observability without inventing another API.

But if the PON Generic Netlink family already identifies the pon_dev,
then OMCI registration plus TX/RX over the same family avoids both a
synthetic omci0 and a new AF_OMCI address family.

I would still like observability to be a first-class requirement.

Does the planned OMCI netlink transport allow one or more listeners to
monitor the raw RX/TX OMCI PDU stream, independently of the daemon
which owns the active OMCI endpoint?

Being able to capture the actual OMCI exchange is important when
debugging OLT interoperability and vendor/private MEs.

I think libpcap/Wireshark integration can be solved separately from the
transport ABI, but we should make sure the kernel transport does not
make passive observation unnecessarily difficult.

 > If all of this sounds plausible, I can post an RFC series of the
 > subsystem within a week, together with a link to a git repository
 > with pon-tool.

Yes, please do.

If possible, I would prefer the initial RFC series to be deliberately
small: enough of pon_dev, the Generic Netlink/YAML model and one real
hardware consumer to review the abstraction, with the complete working
tree and pon-tool available separately in a repository.

That would let us review the generic object model without requiring
the whole working AN7581 implementation to be accepted as one unit.

Christian, John mentioned that you and Airoha have already been
working on this design for quite some time.  It would be very useful
to hear which parts of the current model you consider inherent to PON
and which parts were driven specifically by AN7581 hardware.

It would also help a lot if the design could be independently tested
by developers outside the existing implementation team.

If Airoha is willing to make a small number of AN7581/AN7583
EVK/reference boards and the necessary programming documentation
available, Ziyou and I would be interested in testing the RFC and
comparing it with the existing userspace implementation.

Procurement and any NDA required for register-level documentation can
of course be handled off-list.  For upstream review, though, it would
be very helpful if the interface-level hardware behaviour needed to
justify the Linux abstraction could eventually be documented publicly
or at least be publicly discussable.

If there is an Airoha engineer who is directly responsible for the
PON MAC/SDK side, please feel free to add them to the discussion as
well.

Thanks,
Gaoyang

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-28  8:37               ` John Crispin
  2026-09-28 10:54                 ` Benjamin Larsson
  2026-09-28 11:23                 ` Gaoyang Wei
@ 2026-09-28 14:05                 ` Andrew Lunn
  2 siblings, 0 replies; 26+ messages in thread
From: Andrew Lunn @ 2026-09-28 14:05 UTC (permalink / raw)
  To: John Crispin
  Cc: Ziyou Xu, netdev, Matheus Sampaio Queiroga, Benjamin Larsson,
	Lorenzo Bianconi, Andrew Lunn, Russell King, Gaoyang Wei

On Mon, Sep 28, 2026 at 10:37:48AM +0200, John Crispin wrote:
> Hi all,
> 
> Christian Marangi has been working on the design of a PON subsystem
> together with Airoha for almost a year. I started implementing it on
> the AN7581 two months ago. Reading this thread was a bit of a
> surprise, because the design you are converging on is very close to
> what we already have running

That is good to hear. We are probably not all wrong :-)

	Andrew

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-28 10:54                 ` Benjamin Larsson
@ 2026-09-28 14:10                   ` Andrew Lunn
  2026-10-01 12:25                     ` Benjamin Larsson
  0 siblings, 1 reply; 26+ messages in thread
From: Andrew Lunn @ 2026-09-28 14:10 UTC (permalink / raw)
  To: Benjamin Larsson
  Cc: John Crispin, Ziyou Xu, netdev, Matheus Sampaio Queiroga,
	Lorenzo Bianconi, Andrew Lunn, Russell King, Gaoyang Wei

> In the case of ANI-G 263 I looked at hwmon a while ago and IIRC there was no
> nice way to consume hwmon information from inside the kernel

Not my subsystem, but that surprises me. How is temperature management
performed, where you need to reduce clocks speeds if the system is
getting too warm. Fan control is a similar problem. Both need access
to temperature sensor values.

	Andrew

^ permalink raw reply	[flat|nested] 26+ messages in thread

* Re: [RFC] net: towards a generic PON framework
  2026-09-28 14:10                   ` Andrew Lunn
@ 2026-10-01 12:25                     ` Benjamin Larsson
  0 siblings, 0 replies; 26+ messages in thread
From: Benjamin Larsson @ 2026-10-01 12:25 UTC (permalink / raw)
  To: Andrew Lunn
  Cc: John Crispin, Ziyou Xu, netdev, Matheus Sampaio Queiroga,
	Lorenzo Bianconi, Andrew Lunn, Russell King, Gaoyang Wei

Hi.

On 9/28/26 16:10, Andrew Lunn wrote:
>> In the case of ANI-G 263 I looked at hwmon a while ago and IIRC there was no
>> nice way to consume hwmon information from inside the kernel
> Not my subsystem, but that surprises me. How is temperature management
> performed, where you need to reduce clocks speeds if the system is
> getting too warm. Fan control is a similar problem. Both need access
> to temperature sensor values.
>
> 	Andrew

I only looked at this api briefly. From the api function names and 
documentation it looked like the main direction was towards user space. 
I still postulate that for in-kernel omci ME handling some kernel work 
would be needed to get access to some data, data that might be more easy 
to get access to from user space.

But as I have written 0 omci stacks this is just my general feeling. And 
there is lots of insanity in the standards covering omci, you dont need 
to implement everything just because its in the standard.

Anyway, I'll focus all my efforts into reviewing/testing the RFC coming 
from John and Christian.

MvH

Benjamin Larsson


^ permalink raw reply	[flat|nested] 26+ messages in thread

end of thread, other threads:[~2026-10-01 12:25 UTC | newest]

Thread overview: 26+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-26 17:46 [RFC] net: towards a generic PON framework Gaoyang Wei
2026-09-26 19:28 ` Matheus Sampaio Queiroga
2026-09-26 20:21   ` Gaoyang Wei
2026-09-26 21:55     ` Matheus Sampaio Queiroga
2026-09-26 22:15       ` Gaoyang Wei
2026-09-27  0:05         ` Matheus Sampaio Queiroga
2026-09-27  4:38           ` Gaoyang Wei
2026-09-27 17:47             ` Matheus Sampaio Queiroga
2026-09-27 18:27               ` Gaoyang Wei
2026-09-27 16:57 ` Andrew Lunn
2026-09-27 17:22   ` Gaoyang Wei
2026-09-27 19:24     ` Ziyou Xu
2026-09-27 19:47     ` Andrew Lunn
2026-09-27 20:29       ` Ziyou Xu
2026-09-27 22:30         ` Andrew Lunn
2026-09-27 22:49         ` Andrew Lunn
2026-09-27 23:11           ` Gaoyang Wei
2026-09-28  1:25             ` Andrew Lunn
2026-09-28  8:37               ` John Crispin
2026-09-28 10:54                 ` Benjamin Larsson
2026-09-28 14:10                   ` Andrew Lunn
2026-10-01 12:25                     ` Benjamin Larsson
2026-09-28 11:23                 ` Gaoyang Wei
2026-09-28 14:05                 ` Andrew Lunn
2026-09-27 20:47 ` Benjamin Larsson
2026-09-27 20:58   ` Gaoyang Wei

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox