Initial support for IEEE 1588/802.1AS PTP/gPTP

Remaining work:
- phc2sys YANG model (infix-phc2sys.yang, instance-index + servo params)
- ts2phc YANG model (GPS/PPS → PHC → ptp4l GM path)
- timemaster coordination (Phase 3, after phc2sys YANG is stable)
- show ptp network (YANG action or background-polled topology container)
- CMLDS (requires upstream linuxptp + 802.1ASdm foundation)
- Full 12-bit sdoId, fault log, performance monitoring

Backported patches from linuxptp master:
- port: fix unicast negotiation recovery after FAULT_DETECTED
- udp: fix port-specific ptp/p2p_dst_ipv4 configuration
- pmc: avoid race conditions in agent update
- phc2sys: wait until pmc agent is subscribed (startup race)
- fix MAC driver incorrect SIOCGHWTSTAMP adjustment flags
- pmc_agent: longer update interval when not subscribed
- phc2sys: don't disable pmc agent with -s/-d/-w options
- port_signaling: respect ptp_minor_version in message header
- port: refresh link status on faults
- uds: copy server socket ownership in pmc clients (non-root pmc)
- uds: don't call chmod() on client socket
- port: allow mixing wildcard and exact clock identities
- Add pidfile support to ptp4l, phc2sys, and timemaster

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-04-23 14:19:09 +02:00
parent 73b6e5cc91
commit 45b06dd670
44 changed files with 9095 additions and 4 deletions
@@ -0,0 +1,3 @@
service <!> name:ptp4l :%i log:prio:daemon,tag:ptp4l-%i \
[2345] ptp4l -f /etc/linuxptp/ptp4l-%i.conf \
-- PTP instance %i
@@ -567,6 +567,59 @@ def probe_wifi_radios(out):
out["wifi-radios"].append(info)
def probe_ptp_capabilities(out):
"""Probe PTP timestamping capabilities per physical interface via ethtool --json -T.
Only physical interfaces (those with a 'device' sysfs symlink) are probed;
virtual interfaces such as bridges, VLANs, and tun/tap devices are skipped.
Results are stored under out["interfaces"][<ifname>]["ptp-capabilities"].
"""
net_base = "/sys/class/net"
if not os.path.exists(net_base):
return
ifaces = {}
for ifname in sorted(os.listdir(net_base)):
if ifname == "lo":
continue
# Physical interfaces have a 'device' symlink; virtual ones do not.
if not os.path.exists(os.path.join(net_base, ifname, "device")):
continue
try:
result = subprocess.run(
["ethtool", "--json", "-T", ifname],
capture_output=True, text=True, timeout=5
)
if result.returncode != 0:
continue
data = json.loads(result.stdout)[0]
except Exception:
continue
caps = {
"capabilities": data.get("capabilities", []),
"tx-types": data.get("tx-types", []),
"rx-filters": data.get("rx-filters", []),
}
# phc-index is -1 when no PHC is present; omit in that case.
phc = data.get("phc-index", -1)
if phc >= 0:
caps["phc-index"] = phc
# hwtstamp provider fields are present only on newer kernels/hardware.
if (idx := data.get("hwtstamp-provider-index")) is not None:
caps["hwtstamp-provider-index"] = idx
if (qual := data.get("hwtstamp-provider-qualifier")) is not None:
caps["hwtstamp-provider-qualifier"] = qual
ifaces[ifname] = {"ptp-capabilities": caps}
if ifaces:
out.setdefault("interfaces", {}).update(ifaces)
def main():
out = {
"vendor": None,
@@ -593,6 +646,7 @@ def main():
return err
probe_wifi_radios(out)
probe_ptp_capabilities(out)
if not out["factory-password-hash"]:
sys.stdout.write("\n\n\033[31mCRITICAL BOOTSTRAP ERROR\n" +
+1
View File
@@ -75,6 +75,7 @@ BR2_PACKAGE_IPERF3=y
BR2_PACKAGE_IPROUTE2=y
BR2_PACKAGE_IPTABLES_NFTABLES=y
BR2_PACKAGE_IPUTILS=y
BR2_PACKAGE_LINUXPTP=y
BR2_PACKAGE_LLDPD=y
BR2_PACKAGE_MSTPD=y
BR2_PACKAGE_MTR=y
+1
View File
@@ -68,6 +68,7 @@ BR2_PACKAGE_FRR=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
BR2_PACKAGE_IPROUTE2=y
BR2_PACKAGE_IPUTILS=y
BR2_PACKAGE_LINUXPTP=y
BR2_PACKAGE_LLDPD=y
BR2_PACKAGE_MSTPD=y
BR2_PACKAGE_NETCALC=y
+1
View File
@@ -74,6 +74,7 @@ BR2_PACKAGE_FRR=y
BR2_PACKAGE_IPERF3=y
BR2_PACKAGE_IPROUTE2=y
BR2_PACKAGE_IPUTILS=y
BR2_PACKAGE_LINUXPTP=y
BR2_PACKAGE_LLDPD=y
BR2_PACKAGE_MSTPD=y
BR2_PACKAGE_MTR=y
+1
View File
@@ -70,6 +70,7 @@ BR2_PACKAGE_FRR=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
BR2_PACKAGE_IPROUTE2=y
BR2_PACKAGE_IPUTILS=y
BR2_PACKAGE_LINUXPTP=y
BR2_PACKAGE_LLDPD=y
BR2_PACKAGE_MSTPD=y
BR2_PACKAGE_NETCALC=y
+1
View File
@@ -85,6 +85,7 @@ BR2_PACKAGE_IPERF3=y
BR2_PACKAGE_IPROUTE2=y
BR2_PACKAGE_IPTABLES_NFTABLES=y
BR2_PACKAGE_IPUTILS=y
BR2_PACKAGE_LINUXPTP=y
BR2_PACKAGE_LLDPD=y
BR2_PACKAGE_MSTPD=y
BR2_PACKAGE_MTR=y
+1
View File
@@ -74,6 +74,7 @@ BR2_PACKAGE_IPERF3=y
BR2_PACKAGE_IPROUTE2=y
BR2_PACKAGE_IPTABLES_NFTABLES=y
BR2_PACKAGE_IPUTILS=y
BR2_PACKAGE_LINUXPTP=y
BR2_PACKAGE_LLDPD=y
BR2_PACKAGE_MSTPD=y
BR2_PACKAGE_MTR=y
+1
View File
@@ -67,6 +67,7 @@ BR2_PACKAGE_FRR=y
# BR2_PACKAGE_IFUPDOWN_SCRIPTS is not set
BR2_PACKAGE_IPROUTE2=y
BR2_PACKAGE_IPUTILS=y
BR2_PACKAGE_LINUXPTP=y
BR2_PACKAGE_LLDPD=y
BR2_PACKAGE_MSTPD=y
BR2_PACKAGE_NETCALC=y
+461
View File
@@ -0,0 +1,461 @@
# PTP — Precision Time Protocol
The Precision Time Protocol (PTP), defined in IEEE 1588-2019, synchronises
clocks across a network to sub-microsecond accuracy. Where NTP (Network Time
Protocol) aims at millisecond accuracy over wide-area networks, PTP is
designed for local-area networks and relies on hardware timestamping in the
network interface to eliminate software-induced jitter.
PTP works by exchanging timestamped messages between devices. A *grandmaster
clock* — elected by the **Best TimeTransmitter Clock Algorithm (BTCA)** based
on priority, clock class, and accuracy — distributes time to the rest of the
network. Each synchronising device measures the one-way message delay to its
time-transmitter and continuously adjusts its local clock to compensate.
> [!NOTE]
> The IEEE 1588g-2022 amendment to IEEE 1588-2019 introduced the terms
> *timeTransmitter* and *timeReceiver* as replacements for the former
> *master* and *slave* terminology, and *Best TimeTransmitter Clock
> Algorithm (BTCA)* in place of *BMCA*. This document uses the updated
> terms throughout. You may even see the short forms transmitter and
> receiver here and in online documentation.
## Clock roles
Every device in a PTP network takes one of the following roles:
| Role | Description |
|----------------------------|---------------------------------------------------------------------------------------------|
| **Grandmaster (GM)** | Network-wide time source; elected by BTCA |
| **Time-transmitter** | Sends Sync messages downstream on a port |
| **Time-receiver** | Synchronises to a time-transmitter on a port |
| **Boundary Clock (BC)** | Terminates PTP on each port; acts as time-receiver upstream and time-transmitter downstream |
| **Transparent Clock (TC)** | Passes PTP messages while correcting the residence-time delay accumulated in the device |
An **Ordinary Clock (OC)** has a single PTP port and is either a
time-transmitter (acting as a grandmaster candidate) or a time-receiver
(a leaf node synchronising to the network).
## PTP profiles
A **PTP profile** (as defined in IEEE 1588-2019 §3.1) is a document that
specifies a consistent set of required, permitted, and prohibited PTP
options for a particular application domain — much like a dialect of the
protocol. Examples from the standards world include profiles for power
utilities (IEC/IEEE C37.238), telecom (ITU-T G.8265.1), and
Time-Sensitive Networks.
Each profile sets a unique value in the `majorSdoId` field of PTP message
headers — a 4-bit identifier that lets devices distinguish traffic belonging
to different profiles on the same link. Profile also determines the network
transport (UDP or Ethernet) and the delay measurement mechanism.
Currently, two profiles are supported via the `profile` leaf in `default-ds`:
| `profile` | Standard | majorSdoId | Transport | Delay |
|----------------------|-------------------|:----------:|-----------|----------------|
| `ieee1588` (default) | IEEE 1588-2019 | `0x0` | UDP/IPv4 | `e2e` or `p2p` |
| `ieee802-dot1as` | IEEE 802.1AS-2020 | `0x1` | L2 | `p2p` |
The **gPTP** (generalized Precision Time Protocol) profile from IEEE 802.1AS-2020
is used in **TSN** (Time-Sensitive Networking) and **AVB** (Audio/Video Bridging)
applications. Setting `profile ieee802-dot1as` applies all protocol-mandatory
settings automatically — Layer 2 transport, P2P delay measurement, 802.1AS
multicast addressing, path trace, follow-up information, and neighbour propagation
delay thresholds. The user still configures `priority1`, `priority2`,
`domain-number`, `time-receiver-only`, and timer interval leaves.
The `ieee1588` profile leaves transport and delay mechanism user-configurable
per port.
## Delay mechanisms
PTP measures the link delay between neighbours using one of two mechanisms:
- **End-to-End (E2E)**: Each time-receiver measures the delay to the
grandmaster by sending a `DELAY_REQ` message upstream. Simple to
configure; works with any network topology.
- **Peer-to-Peer (P2P)**: Each port measures its delay to its *immediate
neighbour* independently using `PDELAY_REQ` messages. Enables faster
path-delay updates and is required by the gPTP profile.
## Data Sets
IEEE 1588 organises protocol state into named **Data Sets (DS)** — each a
collection of related attributes for one aspect of a PTP instance. You
will encounter these directly in the CLI and in the `show ptp` output:
| Data Set | CLI node | Contents |
|------------------|----------------|----------------------------------------------------------|
| Default DS | `default-ds` | Instance identity, clock class, priority, domain number |
| Current DS | `current-ds` | Live offset-from-GM, mean path delay, steps-removed |
| Parent DS | `parent-ds` | Grandmaster identity and quality attributes |
| Time Properties DS | `time-properties-ds` | UTC offset, leap-second flags, time source |
| Port DS | `port-ds` | Per-port state, delay mechanism, message intervals |
## Domains
A **PTP domain** (0255) is a logical partition of the network. Devices
only synchronise with others in the same domain. Running multiple
instances on the same device — one per domain, or one per profile — is
fully supported; each instance is independent.
Each PTP instance is identified on the network by its
`(domain-number, profile)` pair, which must be unique across all instances
on a device.
> [!NOTE]
> The `show ptp` offset values reflect **PHC** (PTP Hardware Clock)
> synchronisation only. A PHC is the hardware clock exposed by the network
> interface; it tracks the PTP grandmaster but is independent of the Linux
> system clock, which currently is **not** automatically adjusted.
## Ordinary Clock (time-receiver)
A typical time-receiver Ordinary Clock, synchronising on interface
`eth0` using the default IEEE 1588 profile:
<pre class="cli"><code>admin@example:/> <b>configure</b>
admin@example:/config/> <b>edit ptp instance 0</b>
admin@example:/config/ptp/instance/0/> <b>set default-ds domain-number 0</b>
admin@example:/config/ptp/instance/0/> <b>set default-ds time-receiver-only true</b>
admin@example:/config/ptp/instance/0/> <b>edit port 1</b>
admin@example:/config/ptp/…/0/port/1/> <b>set underlying-interface eth0</b>
admin@example:/config/ptp/…/0/port/1/> <b>leave</b>
</code></pre>
## Ordinary Clock (time-transmitter / grandmaster)
A grandmaster clock with high priority, domain 0:
<pre class="cli"><code>admin@example:/config/> <b>edit ptp instance 0</b>
admin@example:/config/ptp/instance/0/> <b>set default-ds domain-number 0</b>
admin@example:/config/ptp/instance/0/> <b>set default-ds priority1 1</b>
admin@example:/config/ptp/instance/0/> <b>set default-ds priority2 1</b>
admin@example:/config/ptp/instance/0/> <b>edit port 1</b>
admin@example:/config/ptp/…/0/port/1/> <b>set underlying-interface eth0</b>
admin@example:/config/ptp/…/0/port/1/> <b>leave</b>
</code></pre>
Lower `priority1` values win in the BTCA. A clock with `priority1 1` will
be preferred over the default `128` in any compliant network.
## Boundary Clock
A Boundary Clock terminates PTP on each port and re-originates it. Add one
port per interface:
<pre class="cli"><code>admin@example:/config/> <b>edit ptp instance 0</b>
admin@example:/config/ptp/instance/0/> <b>set default-ds instance-type bc</b>
admin@example:/config/ptp/instance/0/> <b>set default-ds domain-number 0</b>
admin@example:/config/ptp/instance/0/> <b>edit port 1</b>
admin@example:/config/ptp/…/0/port/1/> <b>set underlying-interface eth0</b>
admin@example:/config/ptp/…/0/port/1/> <b>end</b>
admin@example:/config/ptp/instance/0/> <b>edit port 2</b>
admin@example:/config/ptp/…/0/port/2/> <b>set underlying-interface eth1</b>
admin@example:/config/ptp/…/0/port/2/> <b>leave</b>
</code></pre>
> [!TIP]
> PTP port numbers are assigned sorted by `port-index`, so `port-index 1`
> becomes PTP port 1, `port-index 2` becomes PTP port 2, and so on.
## Transparent Clock
Transparent Clocks correct timestamps end-to-end without terminating PTP.
Use `instance-type p2p-tc` for a P2P TC (preferred in TSN networks) or
`instance-type e2e-tc` for an E2E TC:
<pre class="cli"><code>admin@example:/config/> <b>edit ptp instance 0</b>
admin@example:/config/ptp/instance/0/> <b>set default-ds instance-type p2p-tc</b>
admin@example:/config/ptp/instance/0/> <b>set default-ds domain-number 0</b>
admin@example:/config/ptp/instance/0/> <b>edit port 1</b>
admin@example:/config/ptp/…/0/port/1/> <b>set underlying-interface eth0</b>
admin@example:/config/ptp/…/0/port/1/> <b>end</b>
admin@example:/config/ptp/instance/0/> <b>edit port 2</b>
admin@example:/config/ptp/…/0/port/2/> <b>set underlying-interface eth1</b>
admin@example:/config/ptp/…/0/port/2/> <b>leave</b>
</code></pre>
> [!NOTE]
> For Transparent Clocks the delay mechanism is determined globally by the
> `instance-type` (`p2p-tc` → P2P, `e2e-tc` → E2E). Per-port
> `delay-mechanism` settings have no effect for TC instances.
## gPTP / IEEE 802.1AS
The gPTP profile is used in TSN and AVB applications. Setting
`profile ieee802-dot1as` applies all protocol-mandatory options from
IEEE 802.1AS-2020 automatically — Layer 2 transport, P2P delay
measurement, 802.1AS multicast addressing, and related protocol features.
<pre class="cli"><code>admin@example:/config/> <b>edit ptp instance 0</b>
admin@example:/config/ptp/instance/0/> <b>set default-ds profile ieee802-dot1as</b>
admin@example:/config/ptp/instance/0/> <b>set default-ds domain-number 0</b>
admin@example:/config/ptp/instance/0/> <b>set default-ds time-receiver-only true</b>
admin@example:/config/ptp/instance/0/> <b>edit port 1</b>
admin@example:/config/ptp/…/0/port/1/> <b>set underlying-interface eth0</b>
admin@example:/config/ptp/…/0/port/1/> <b>leave</b>
</code></pre>
> [!NOTE]
> The `ieee802-dot1as` profile enforces Layer 2 transport and P2P delay
> measurement globally, as required by IEEE 802.1AS-2020. Per-port
> `delay-mechanism` settings have no effect for 802.1AS instances.
## Multiple Instances
Multiple PTP instances can run simultaneously, one per domain or profile
combination. Each instance must have a unique `(domain-number, profile)`
pair and an independent set of ports:
<pre class="cli"><code>admin@example:/config/> <b>edit ptp instance 0</b>
admin@example:/config/ptp/instance/0/> <b>set default-ds domain-number 0</b>
admin@example:/config/ptp/instance/0/> <b>set default-ds profile ieee1588</b>
admin@example:/config/ptp/instance/0/> <b>edit port 1</b>
admin@example:/config/ptp/…/0/port/1/> <b>set underlying-interface eth0</b>
admin@example:/config/ptp/…/0/port/1/> <b>end</b>
admin@example:/config/ptp/instance/0/> <b>end</b>
admin@example:/config/ptp/> <b>edit instance 1</b>
admin@example:/config/ptp/instance/1/> <b>set default-ds domain-number 0</b>
admin@example:/config/ptp/instance/1/> <b>set default-ds profile ieee802-dot1as</b>
admin@example:/config/ptp/instance/1/> <b>edit port 1</b>
admin@example:/config/ptp/…/1/port/1/> <b>set underlying-interface eth1</b>
admin@example:/config/ptp/…/1/port/1/> <b>leave</b>
</code></pre>
## Port states
Each PTP port progresses through a state machine. The current state is
shown in the `show ptp` port table:
| State | Meaning |
|------------------------|----------------------------------------------------------------|
| `initializing` | Port is starting up, not yet ready to exchange messages |
| `faulty` | A fault condition has been detected on this port |
| `disabled` | Port is administratively disabled |
| `listening` | Awaiting `ANNOUNCE` messages; BTCA has not yet resolved |
| `pre-time-transmitter` | Transitioning towards time-transmitter state |
| `time-transmitter` | Port is acting as time-transmitter on this link |
| `passive` | Another port on this device is already time-transmitter |
| `uncalibrated` | Receiving sync; local clock not yet locked to time-transmitter |
| `time-receiver` | Port is locked and tracking its time-transmitter |
A port in `uncalibrated` will typically transition to `time-receiver`
within a few seconds once the clock servo has converged.
## Monitoring
> [!TIP] Use the ++question++ key in the CLI
> The `show ptp` command has sub-commands — tap ++question++ after
> `show ptp` to see them, or use ++tab++ to complete.
### Show all PTP instances
<pre class="cli"><code>admin@example:/> <b>show ptp</b>
<b>PTP Instance 0</b> Ordinary Clock · domain 0
────────────────────────────────────────────────────────────────────
Clock identity : AA-BB-CC-FF-FE-00-11-22
Grandmaster : DD-EE-FF-FF-FE-33-44-55
Priority1/Priority2 : 128 / 128
GM Priority1/Priority2 : 1 / 1
Clock class : cc-time-receiver-only
GM clock class : cc-primary-sync
Time source : gnss
PTP timescale : yes
UTC offset : 37 s
Time traceable : yes
Freq. traceable : yes
Offset from GM : -42 ns
Mean path delay : 1250 ns
Steps removed : 1
────────────────────────────────────────────────────────────────────
Ports
<span class="header">PORT INTERFACE STATE DELAY LINK DELAY (ns)</span>
1 eth0 <span class="ok">time-receiver</span> E2E 0
────────────────────────────────────────────────────────────────────
Message Statistics (▼ rx ▲ tx)
<span class="header">PORT INTERFACE SYNC ▼ SYNC ▲ ANN ▼ ANN ▲ PD ▼ PD ▲</span>
1 eth0 42 0 15 0 0 0
</code></pre>
Port state is colour-coded: green for `time-transmitter` and `time-receiver`
(actively synchronising), yellow for transient states (`listening`,
`uncalibrated`, `pre-time-transmitter`), and red for fault states (`faulty`,
`disabled`). The *Message Statistics* section is omitted when no counts are
available.
### Show a specific instance
<pre class="cli"><code>admin@example:/> <b>show ptp 0</b>
</code></pre>
## Tuning port intervals
Adjust announcement, sync, and delay-request intervals per port. Values
are expressed as log₂ of the interval in seconds (e.g. `-3` = 125 ms,
`0` = 1 s, `1` = 2 s):
<pre class="cli"><code>admin@example:/config/ptp/…/0/port/1/> <b>set port-ds log-announce-interval 0</b>
admin@example:/config/ptp/…/0/port/1/> <b>set port-ds log-sync-interval -3</b>
admin@example:/config/ptp/…/0/port/1/> <b>set port-ds log-min-delay-req-interval 0</b>
admin@example:/config/ptp/…/0/port/1/> <b>set announce-receipt-timeout 3</b>
</code></pre>
`announce-receipt-timeout` is a count of announce intervals, not a duration
in seconds. With `log-announce-interval 0` (1 s) and
`announce-receipt-timeout 3`, a port waits 3 s without receiving an
`ANNOUNCE` before declaring the time-transmitter lost and returning to
`listening`.
## Message exchange
PTP distributes time using a small set of messages, all of which carry
hardware timestamps at the network interface:
| Message | Timestamped | Purpose |
|-------------------------|:-----------:|-----------------------------------------------------|
| `ANNOUNCE` | No | Advertises clock quality for BTCA election |
| `SYNC` | Yes | Carries transmitter timestamp to receivers |
| `FOLLOW_UP` | No | Carries precise `t1` in two-step mode |
| `DELAY_REQ` | Yes | Receiver-initiated E2E delay measurement |
| `DELAY_RESP` | No | Time-transmitter reply to `DELAY_REQ` |
| `PDELAY_REQ` | Yes | Initiates P2P neighbour-delay measurement |
| `PDELAY_RESP` | Yes | Neighbour reply to `PDELAY_REQ` |
| `PDELAY_RESP_FOLLOW_UP` | No | Carries precise `PDELAY_RESP` `t3` in two-step mode |
In **one-step** mode the timestamp is embedded directly into each `SYNC`
message as it leaves the wire, eliminating the need for `FOLLOW_UP`.
In **two-step** mode the `SYNC` carries a placeholder and the precise
transmit timestamp arrives in a subsequent `FOLLOW_UP`. Hardware
timestamping gives high accuracy in both modes; one-step reduces message
overhead at the cost of more demanding hardware support.
## Message format
Every PTP message begins with a common 34-octet header, regardless of type.
The structure below follows the traditional IETF bit-field layout: each row
is four octets wide, bit 7 (MSB) is on the left and bit 0 (LSB) on the
right within each octet.
```
7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
0-3 |trSpec |msgType| rsv | ver | messageLength |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
4-7 | domainNumber | minorSdoId | flags |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
8-15 | |
+ correctionField +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
16-19 | messageTypeSpecific |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20-27 | |
+ clockIdentity +
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
28-31 | portNumber | sequenceId |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
32-33 | controlField | logMsgIntvl |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
```
- **`trSpec`** (`transportSpecific`, bits 74 of octet 0): 4-bit profile
identifier. `0x0` = IEEE 1588, `0x1` = gPTP (802.1AS). Set implicitly
by the `profile` configuration leaf.
- **`msgType`** (`messageType`, bits 30 of octet 0): `0x0` SYNC ·
`0x1` DELAY_REQ · `0x2` PDELAY_REQ · `0x3` PDELAY_RESP ·
`0x8` FOLLOW_UP · `0x9` DELAY_RESP · `0xA` PDELAY_RESP_FOLLOW_UP ·
`0xB` ANNOUNCE.
- **`rsv`** (reserved, bits 74 of octet 1): Set to zero; ignored on
receipt.
- **`ver`** (`versionPTP`, bits 30 of octet 1): PTP version; `2` for
IEEE 1588-2008 and IEEE 1588-2019.
- **`messageLength`** (octets 23): Total message length in octets,
including the header.
- **`domainNumber`** (octet 4): PTP domain; receivers silently discard
messages that do not match their configured domain.
- **`minorSdoId`** (octet 5): Reserved in IEEE 1588-2008; carries a
profile sub-identifier in IEEE 1588-2019.
- **`flags`** (octets 67): Per-message flags — includes the two-step
flag (set when a FOLLOW_UP will follow a SYNC), UTC offset valid, and
leap-second indicators.
- **`correctionField`** (octets 815): Accumulated path correction in
nanoseconds × 2¹⁶. Transparent Clocks add their measured residence
time and link delay here as they forward each message, so the final
time-receiver can subtract the total accumulated delay.
- **`messageTypeSpecific`** (octets 1619): Reserved in IEEE 1588-2008;
carries message-type-specific data in IEEE 1588-2019.
- **`clockIdentity`** (octets 2027): EUI-64 identity of the sending
clock — the value shown as "Clock identity" in `show ptp`.
- **`portNumber`** (octets 2829): Port number of the sender within its
clock; together with `clockIdentity` it forms the unique
`sourcePortIdentity`.
- **`sequenceId`** (octets 3031): Increments with each message; used to
match a DELAY_REQ to its DELAY_RESP.
- **`controlField`** (octet 32): Deprecated in PTPv2; set to fixed
values per message type for backward compatibility with PTPv1.
- **`logMsgIntvl`** (`logMessageInterval`, octet 33): Log₂ of the
expected interval between messages of this type; `0x7F` means not
applicable.
The `transportSpecific` and `domainNumber` fields are the quickest way to
verify on the wire that a device is using the profile and domain you
configured.
### Decoding with Wireshark
Wireshark decodes PTP messages automatically, expanding every header field
and message-type-specific payload in the packet tree. PTP travels over
two UDP ports — 319 for event messages (SYNC, DELAY_REQ, PDELAY_REQ and
their responses) and 320 for general messages (ANNOUNCE, FOLLOW_UP) — as
well as directly over Ethernet (EtherType `0x88F7`) when layer-2 transport
is in use.
Use the display filter `ptp` to isolate PTP traffic:
```
ptp
```
To narrow down to a specific domain or profile (exact field names can be
verified in Wireshark via **View → Internals → Supported Protocols**,
filtering for `ptp`):
```
ptp.v2.domainnumber == 0
ptp.v2.transportspecific == 1
```
This makes it straightforward to confirm which grandmaster a port is
tracking, verify that `correctionField` is being updated by a Transparent
Clock, or diagnose why the BTCA is not electing the expected grandmaster.
## Glossary
| Abbreviation | Expansion | Notes |
|--------------|--------------------------------------|-------------------------------------------------------------------|
| AVB | Audio/Video Bridging | IEEE 802.1 precursor to TSN; real-time AV over Ethernet |
| IETF | Internet Engineering Task Force | Standards body; defines RFC for layer-3 and up |
| UDP | User Datagram Protocol | IP transport used by PTP; port 319 (event) and 320 (general) |
| EUI-64 | Extended Unique Identifier (64-bit) | IEEE identifier format used as `clockIdentity` in PTP |
| EtherType | Ethernet frame type field | `0x88F7` identifies PTP over layer-2 Ethernet |
| BC | Boundary Clock | Terminates and re-originates PTP on each port |
| BTCA | Best TimeTransmitter Clock Algorithm | Elects the GM; replaces BMCA from IEEE 1588-2008 |
| CMLDS | Common Mean Link Delay Service | IEEE 1588-2019 §16.6; shared delay service for multiple instances |
| DS | Data Set | Named attribute collection in IEEE 1588 (default-ds, port-ds, …) |
| E2E | End-to-End | Delay mechanism: measures path from GM to time-receiver |
| GM | Grandmaster | PTP network-wide time source, elected by BTCA |
| gPTP | generalized Precision Time Protocol | IEEE 802.1AS profile; used in TSN and AVB |
| NTP | Network Time Protocol | Millisecond-accuracy time protocol for wide-area use |
| OC | Ordinary Clock | Single-port PTP clock; time-transmitter or time-receiver |
| P2P | Peer-to-Peer | Delay mechanism: measures delay to immediate neighbour |
| PHC | PTP Hardware Clock | Hardware clock in the NIC used for PTP timestamping |
| PTP | Precision Time Protocol | IEEE 1588 sub-microsecond clock synchronisation protocol |
| SDO | Standards Development Organization | Body that defines a PTP profile; encoded in `sdo-id` |
| TC | Transparent Clock | Forwards PTP messages, correcting for residence-time delay |
| TSN | Time-Sensitive Networking | IEEE 802.1 standard set for deterministic Ethernet |
+1
View File
@@ -47,6 +47,7 @@ nav:
- Device Discovery: discovery.md
- DHCP Server: dhcp.md
- NTP Server: ntp.md
- PTP (IEEE 1588/802.1AS): ptp.md
- System:
- Boot Procedure: boot.md
- Configuration: system.md
@@ -0,0 +1,114 @@
From d68e6a149340d987ebd19f86a7d792f4aaefdf98 Mon Sep 17 00:00:00 2001
From: Vincent Cheng <vscheng@gmail.com>
Date: Tue, 17 Sep 2024 15:23:54 -0400
Subject: [PATCH 01/13] port: Fix unicast negotiation doesn't recover after
FAULT_DETECTED
Organization: Wires
_Problem_
After a port link down/up or a tx_timestamp timeout issue, a port acting
as unicast master does not issue ANNC messages after granting unicast
request for ANNC.
_Analysis_
When a port FAULT occurs, the port transitions to FAULTY on FAULT_DETECTED
and subsequently port_disable(p) and port_initialize(p) are called on port recovery.
A port acting as a unicast master, stores clients in p->unicast_service->queue.
When a port receives a unicast request, unicast_service_add() is called.
In unicast_service_add(), if the request does not match an entry in
p->unicast_service->queue, FD_UNICAST_SRV_TIMER is started via
unicast_service_rearm_timer().
If the unicast request matches an existing p->unicast_service->queue entry
the request is considered an extension and FD_UNICAST_SRV_TIMER must
already be running.
port_disable() clears FD_UNICAST_SRV_TIMER, ie. stops FD_UNICAST_SRV_TIMER.
However, port_disable() does not clear p->unicast_service->queue.
When the port is restarted, the port retains the previous client data.
After port recovery, when the client attempts to restart the unicast
service, the request matches an existing entry in p->unicast_service->queue,
and so FD_UNICAST_SRV_TIMER is not started because the port expected
that the FD_UNICAST_SRV_TIMER is already running.
_Fix_
This patch clears the unicast client data in port_disable() so
that upon recovery, the initial unicast request will be considered
a new request and trigger the start of the FD_UNICAST_SRV_TIMER.
v2:
- Add missing sign-off
- Send to develop-request instead of users list
Signed-off-by: Vincent Cheng <vincent.cheng.xh@renesas.com>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
port.c | 1 +
unicast_service.c | 21 +++++++++++++++++++++
unicast_service.h | 6 ++++++
3 files changed, 28 insertions(+)
diff --git a/port.c b/port.c
index db35a44..282be66 100644
--- a/port.c
+++ b/port.c
@@ -1985,6 +1985,7 @@ void port_disable(struct port *p)
flush_peer_delay(p);
p->best = NULL;
+ unicast_service_clear_clients(p);
free_foreign_masters(p);
transport_close(p->trp, &p->fda);
diff --git a/unicast_service.c b/unicast_service.c
index 687468c..d7a4ecd 100644
--- a/unicast_service.c
+++ b/unicast_service.c
@@ -571,3 +571,24 @@ int unicast_service_timer(struct port *p)
}
return err;
}
+
+void unicast_service_clear_clients(struct port *p)
+{
+ struct unicast_client_address *client, *temp;
+ struct unicast_service_interval *interval;
+
+ if (!p->unicast_service) {
+ return;
+ }
+
+ while ((interval = pqueue_extract(p->unicast_service->queue)) != NULL) {
+
+ LIST_REMOVE(interval, list);
+
+ LIST_FOREACH_SAFE(client, &interval->clients, list, temp) {
+ LIST_REMOVE(client, list);
+ free(client);
+ }
+ free(interval);
+ }
+}
\ No newline at end of file
diff --git a/unicast_service.h b/unicast_service.h
index f0d6487..8ea1a59 100644
--- a/unicast_service.h
+++ b/unicast_service.h
@@ -87,4 +87,10 @@ void unicast_service_remove(struct port *p, struct ptp_message *m,
*/
int unicast_service_timer(struct port *p);
+/**
+ * Clears unicast clients on a given port.
+ * @param p The port in question.
+ */
+void unicast_service_clear_clients(struct port *p);
+
#endif
--
2.43.0
@@ -0,0 +1,92 @@
From 4ea423b94264e4eeb0c2706fc3485b1fe283cc11 Mon Sep 17 00:00:00 2001
From: Miroslav Lichvar <mlichvar@redhat.com>
Date: Wed, 25 Sep 2024 14:37:20 +0200
Subject: [PATCH 02/13] udp: Fix port-specific ptp/p2p_dst_ipv4 configuration.
Organization: Wires
If different ports are configured with a different ptp_dst_ipv4 or
p2p_dst_ipv4 address, only the last port in the configuration works
correctly. This is caused by a global variable holding the
destination address for all ports using the udp transport.
Move the address to the udp structure to avoid the conflict between
different ports, same as when port-specific scope in udp6 was fixed
in commit a48666bee3dd ("udp6: Make mc6_addr transport-local").
Fixes: 8a26c94cc88e ("udp+udp6: Make IP addresses configurable.")
Signed-off-by: Miroslav Lichvar <mlichvar@redhat.com>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
udp.c | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/udp.c b/udp.c
index 38d0ec4..c9b5f39 100644
--- a/udp.c
+++ b/udp.c
@@ -44,6 +44,7 @@ struct udp {
struct transport t;
struct address ip;
struct address mac;
+ struct in_addr mcast_addr[2];
};
static int mcast_bind(int fd, int index)
@@ -146,8 +147,6 @@ no_socket:
enum { MC_PRIMARY, MC_PDELAY };
-static struct in_addr mcast_addr[2];
-
static int udp_open(struct transport *t, struct interface *iface,
struct fdarray *fda, enum timestamp_type ts_type)
{
@@ -165,22 +164,22 @@ static int udp_open(struct transport *t, struct interface *iface,
sk_interface_addr(name, AF_INET, &udp->ip);
str = config_get_string(t->cfg, name, "ptp_dst_ipv4");
- if (!inet_aton(str, &mcast_addr[MC_PRIMARY])) {
+ if (!inet_aton(str, &udp->mcast_addr[MC_PRIMARY])) {
pr_err("invalid ptp_dst_ipv4 %s", str);
return -1;
}
str = config_get_string(t->cfg, name, "p2p_dst_ipv4");
- if (!inet_aton(str, &mcast_addr[MC_PDELAY])) {
+ if (!inet_aton(str, &udp->mcast_addr[MC_PDELAY])) {
pr_err("invalid p2p_dst_ipv4 %s", str);
return -1;
}
- efd = open_socket(name, mcast_addr, EVENT_PORT, ttl);
+ efd = open_socket(name, udp->mcast_addr, EVENT_PORT, ttl);
if (efd < 0)
goto no_event;
- gfd = open_socket(name, mcast_addr, GENERAL_PORT, ttl);
+ gfd = open_socket(name, udp->mcast_addr, GENERAL_PORT, ttl);
if (gfd < 0)
goto no_general;
@@ -223,6 +222,7 @@ static int udp_send(struct transport *t, struct fdarray *fda,
enum transport_event event, int peer, void *buf, int len,
struct address *addr, struct hw_timestamp *hwts)
{
+ struct udp *udp = container_of(t, struct udp, t);
struct address addr_buf;
unsigned char junk[1600];
ssize_t cnt;
@@ -243,8 +243,8 @@ static int udp_send(struct transport *t, struct fdarray *fda,
if (!addr) {
memset(&addr_buf, 0, sizeof(addr_buf));
addr_buf.sin.sin_family = AF_INET;
- addr_buf.sin.sin_addr = peer ? mcast_addr[MC_PDELAY] :
- mcast_addr[MC_PRIMARY];
+ addr_buf.sin.sin_addr = peer ? udp->mcast_addr[MC_PDELAY] :
+ udp->mcast_addr[MC_PRIMARY];
addr_buf.len = sizeof(addr_buf.sin);
addr = &addr_buf;
}
--
2.43.0
@@ -0,0 +1,82 @@
From e76bb37019605dea4acd9ccec620a3faec1ba402 Mon Sep 17 00:00:00 2001
From: Miroslav Lichvar <mlichvar@redhat.com>
Date: Thu, 17 Oct 2024 15:05:21 +0200
Subject: [PATCH 03/13] pmc: Avoid race conditions in agent update.
Organization: Wires
The pmc_agent_update() function updates the subscription to
notifications and also the current UTC offset. It uses a timeout of 0
to avoid blocking. When the pmc client sends the first request, the
response from ptp4l may not come quickly enough to be received in the
same run_pmc() call. It then sends the other request and checks for the
response. If it is the response to the first request, it will be ignored.
The update works correctly only if both responses are quick enough to be
received in the same call, or are both slow enough that they are
received in the next call of the pmc_agent_update() function.
The function needs to be called a random number of times in order to
finish one update. If the mismatch between requests and responses
happened consistently, the agent would never reach the up-to-date state
and phc2sys would not enter the main synchronization loop.
Split the update into two phases, where only one thing is updated at a
time. The function now needs to be called at most 3 times to update both
the subscription and UTC offset, assuming it is not interrupted by
another request outside of the agent's update.
Signed-off-by: Miroslav Lichvar <mlichvar@redhat.com>
Reviewed-by: Jacob Keller <jacob.e.keller@intel.com>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
pmc_agent.c | 24 ++++++++++++++++++------
1 file changed, 18 insertions(+), 6 deletions(-)
diff --git a/pmc_agent.c b/pmc_agent.c
index 86b6ee6..d1a3367 100644
--- a/pmc_agent.c
+++ b/pmc_agent.c
@@ -37,6 +37,7 @@ struct pmc_agent {
struct pmc *pmc;
uint64_t pmc_last_update;
uint64_t update_interval;
+ int update_phase;
struct defaultDS dds;
bool dds_valid;
@@ -427,16 +428,27 @@ int pmc_agent_update(struct pmc_agent *node)
ts = tp.tv_sec * NS_PER_SEC + tp.tv_nsec;
if (ts - node->pmc_last_update >= node->update_interval) {
- if (node->stay_subscribed) {
- renew_subscription(node, 0);
- }
- if (!pmc_agent_query_utc_offset(node, 0)) {
+ switch (node->update_phase) {
+ case 0:
+ if (node->stay_subscribed &&
+ renew_subscription(node, 0))
+ break;
+ node->update_phase++;
+ /* Fall through */
+ case 1:
+ if (pmc_agent_query_utc_offset(node, 0))
+ break;
+ node->update_phase++;
+ /* Fall through */
+ default:
node->pmc_last_update = ts;
+ node->update_phase = 0;
+ break;
}
+ } else {
+ run_pmc(node, 0, -1, &msg);
}
- run_pmc(node, 0, -1, &msg);
-
return 0;
}
--
2.43.0
@@ -0,0 +1,45 @@
From 1942fde263f40d318d56acf824626641263facd0 Mon Sep 17 00:00:00 2001
From: Miroslav Lichvar <mlichvar@redhat.com>
Date: Thu, 17 Oct 2024 15:05:22 +0200
Subject: [PATCH 04/13] phc2sys: Wait until pmc agent is subscribed.
Organization: Wires
When phc2sys is configured with multiple domains, different domains may
have their pmc agent subscribed after different number of calls of the
pmc_agent_update() function depending on how quickly responses from
ptp4l are received. If one domain triggers reconfiguration and the other
domain does not have its agent subscribed yet, it will not have any of
its clocks synchronized until a port changes state and triggers another
reconfiguration of the domain.
To avoid this problem, wait for each domain to have its agent subscribed
before entering the main synchronization loop. Use a 10ms update
interval to speed up the start of phc2sys.
Signed-off-by: Miroslav Lichvar <mlichvar@redhat.com>
Reviewed-by: Jacob Keller <jacob.e.keller@intel.com>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
phc2sys.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/phc2sys.c b/phc2sys.c
index 6113539..47e896e 100644
--- a/phc2sys.c
+++ b/phc2sys.c
@@ -962,6 +962,12 @@ static int auto_init_ports(struct domain *domain)
return -1;
}
+ while (!pmc_agent_is_subscribed(domain->agent)) {
+ usleep(10000);
+ if (pmc_agent_update(domain->agent) < 0)
+ return -1;
+ }
+
for (i = 1; i <= number_ports; i++) {
err = pmc_agent_query_port_properties(domain->agent, 1000, i,
&state, &timestamping,
--
2.43.0
@@ -0,0 +1,51 @@
From 9e1b0df61c4dc9bb1a4aac11acb06e4f1f2c68f7 Mon Sep 17 00:00:00 2001
From: William Comly <william.comly@nav-timing.safrangroup.com>
Date: Thu, 31 Oct 2024 15:10:20 -0400
Subject: [PATCH 05/13] Fix issue where MAC driver is configured with incorrect
adjustment flags sometimes returned by SIOCGHWTSTAMP test.
Organization: Wires
Once the check for the VLAN bonding flag is complete, clear the ifreq
message to ensure only the intended configuration and flags are set
in the driver.
Signed-off-by: William Comly <william.comly@nav-timing.safrangroup.com>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
sk.c | 20 +++++++++-----------
1 file changed, 9 insertions(+), 11 deletions(-)
diff --git a/sk.c b/sk.c
index aadb237..4860af2 100644
--- a/sk.c
+++ b/sk.c
@@ -69,17 +69,15 @@ static int hwts_init(int fd, const char *device, int rx_filter,
/* Test if VLAN over bond is supported. */
cfg.flags = HWTSTAMP_FLAG_BONDED_PHC_INDEX;
err = ioctl(fd, SIOCGHWTSTAMP, &ifreq);
- if (err < 0) {
- /*
- * Fall back without flag if user runs new build on old kernel
- * or if driver does not support SIOCGHWTSTAMP ioctl.
- */
- if (errno == EINVAL || errno == EOPNOTSUPP) {
- init_ifreq(&ifreq, &cfg, device);
- } else {
- pr_err("ioctl SIOCGHWTSTAMP failed: %m");
- return err;
- }
+ if (err < 0 && errno != EINVAL && errno != EOPNOTSUPP) {
+ pr_err("ioctl SIOCGHWTSTAMP failed: %m");
+ return err;
+ }
+
+ init_ifreq(&ifreq, &cfg, device);
+ /* If VLAN over bond supported in kernel, configure flag in driver. */
+ if (err == 0) {
+ cfg.flags = HWTSTAMP_FLAG_BONDED_PHC_INDEX;
}
switch (sk_hwts_filter_mode) {
--
2.43.0
@@ -0,0 +1,50 @@
From 0bb9080c1bf631ae0265475d75860b6acd92ed2c Mon Sep 17 00:00:00 2001
From: Miroslav Lichvar <mlichvar@redhat.com>
Date: Tue, 26 Nov 2024 15:10:32 +0100
Subject: [PATCH 06/13] pmc_agent: Use longer update interval when not
subscribed.
Organization: Wires
When phc2sys is started with the -w option, the pmc agent is not
subscribed to events by the pmc_agent_subscribe() function, which also
sets the update interval. The update interval in this case is zero,
which means the pmc agent is trying to update the currentUtcOffset value
on every call of pmc_agent_update(), i.e. on every clock update in
phc2sys.
Set a default update interval of 60 seconds to reduce the rate of
pmc requests.
Fixes: e3ca7ea90a9e ("pmc_agent: Make update interval configurable.")
Signed-off-by: Miroslav Lichvar <mlichvar@redhat.com>
Reviewed-by: Jacob Keller <jacob.e.keller@intel.com>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
pmc_agent.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/pmc_agent.c b/pmc_agent.c
index d1a3367..663adc0 100644
--- a/pmc_agent.c
+++ b/pmc_agent.c
@@ -33,6 +33,9 @@
#define UPDATES_PER_SUBSCRIPTION 3
#define MIN_UPDATE_INTERVAL 10
+/* Update interval if the agent not subscribed, just polling the UTC offset */
+#define DEFAULT_UPDATE_INTERVAL 60
+
struct pmc_agent {
struct pmc *pmc;
uint64_t pmc_last_update;
@@ -253,6 +256,7 @@ int init_pmc_node(struct config *cfg, struct pmc_agent *node, const char *uds,
}
node->recv_subscribed = recv_subscribed;
node->recv_context = context;
+ node->update_interval = DEFAULT_UPDATE_INTERVAL * NS_PER_SEC;
return 0;
}
--
2.43.0
@@ -0,0 +1,40 @@
From 5a97466a0ed88bae244566c2a5dba85ce72e4f01 Mon Sep 17 00:00:00 2001
From: Miroslav Lichvar <mlichvar@redhat.com>
Date: Tue, 26 Nov 2024 15:10:33 +0100
Subject: [PATCH 07/13] phc2sys: Don't disable pmc agent with -s -d -w options.
Organization: Wires
When phc2sys is started with -s and -d options to combine a PPS device
and PHC device as a time source, but without an offset specified by
the -O option, the pmc agent is disabled after waiting for ptp4l to have
a port in a synchronized state. This prevents phc2sys from following
changes in the currentUtcOffset value.
Disable the pmc agent only if no PHC device is specified by the -s
option, i.e. there are no PHC readings to which the UTC offset could be
applied.
Fixes: 5f1b419c4102 ("phc2sys: Replace magical test with a proper test.")
Signed-off-by: Miroslav Lichvar <mlichvar@redhat.com>
Reviewed-by: Jacob Keller <jacob.e.keller@intel.com>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
phc2sys.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/phc2sys.c b/phc2sys.c
index 47e896e..5962f6c 100644
--- a/phc2sys.c
+++ b/phc2sys.c
@@ -1547,7 +1547,7 @@ int main(int argc, char *argv[])
if (domains[0].forced_sync_offset ||
!phc2sys_using_systemclock(&domains[0]) ||
- hardpps_configured(pps_fd)) {
+ (hardpps_configured(pps_fd) && !src_name)) {
pmc_agent_disable(domains[0].agent);
}
}
--
2.43.0
@@ -0,0 +1,29 @@
From 9822b22a07e05eb087cedfbb4a21ceecbb8ec74e Mon Sep 17 00:00:00 2001
From: William Comly <william.comly@nav-timing.safrangroup.com>
Date: Wed, 19 Feb 2025 10:12:28 -0500
Subject: [PATCH 08/13] port_signaling.c: ensure that signaling messages
respect ptp_minor_version in message header
Organization: Wires
Signed-off-by: William Comly <william.comly@nav-timing.safrangroup.com>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
port_signaling.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/port_signaling.c b/port_signaling.c
index ca4202f..cf28756 100644
--- a/port_signaling.c
+++ b/port_signaling.c
@@ -41,7 +41,7 @@ struct ptp_message *port_signaling_construct(struct port *p,
}
msg->hwts.type = p->timestamping;
msg->header.tsmt = SIGNALING | p->transportSpecific;
- msg->header.ver = PTP_VERSION;
+ msg->header.ver = ptp_hdr_ver;
msg->header.messageLength = sizeof(struct signaling_msg);
msg->header.domainNumber = clock_domain_number(p->clock);
msg->header.sourcePortIdentity = p->portIdentity;
--
2.43.0
@@ -0,0 +1,82 @@
From b8a3020e806dd6252e7edd434ae3940706a0b516 Mon Sep 17 00:00:00 2001
From: Miroslav Lichvar <mlichvar@redhat.com>
Date: Tue, 4 Mar 2025 15:53:37 +0100
Subject: [PATCH 09/13] port: Refresh link status on faults.
Organization: Wires
ptp4l gets the ENOBUFS error on the netlink socket when the kernel has
to drop messages due to full socket buffer. If ptp4l has a port in the
faulty state waiting for the link to go up and that event corresponds
to one of the dropped netlink messages, the port will be stuck in the
faulty state until the link goes down and up again.
To prevent the port from getting stuck, request the current link status
when dispatching the EV_FAULT_DETECTED event. Also, reopen the socket to
get rid of the buffered messages when handling the fault and again when
reinitializing the port.
Signed-off-by: Miroslav Lichvar <mlichvar@redhat.com>
Reviewed-by: Jacob Keller <jacob.e.keller@intel.com>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
port.c | 30 +++++++++++++++++++++++-------
1 file changed, 23 insertions(+), 7 deletions(-)
diff --git a/port.c b/port.c
index 282be66..5eea2f2 100644
--- a/port.c
+++ b/port.c
@@ -1975,6 +1975,20 @@ static int port_cmlds_initialize(struct port *p)
return port_cmlds_renew(p, now.tv_sec);
}
+static void port_rtnl_initialize(struct port *p)
+{
+ /* Reopen the socket to get rid of buffered messages */
+ if (p->fda.fd[FD_RTNL] >= 0) {
+ rtnl_close(p->fda.fd[FD_RTNL]);
+ }
+ p->fda.fd[FD_RTNL] = rtnl_open();
+ if (p->fda.fd[FD_RTNL] >= 0) {
+ rtnl_link_query(p->fda.fd[FD_RTNL], interface_name(p->iface));
+ }
+
+ clock_fda_changed(p->clock);
+}
+
void port_disable(struct port *p)
{
int i;
@@ -2088,13 +2102,8 @@ int port_initialize(struct port *p)
if (p->bmca == BMCA_NOOP) {
port_set_delay_tmo(p);
}
- if (p->fda.fd[FD_RTNL] == -1) {
- p->fda.fd[FD_RTNL] = rtnl_open();
- }
- if (p->fda.fd[FD_RTNL] >= 0) {
- const char *ifname = interface_name(p->iface);
- rtnl_link_query(p->fda.fd[FD_RTNL], ifname);
- }
+
+ port_rtnl_initialize(p);
}
port_nrate_initialize(p);
@@ -3769,6 +3778,13 @@ int port_state_update(struct port *p, enum fsm_event event, int mdiff)
if (port_link_status_get(p) && clear_fault_asap(&i)) {
pr_notice("%s: clearing fault immediately", p->log_name);
next = p->state_machine(next, EV_FAULT_CLEARED, 0);
+ } else if (event == EV_FAULT_DETECTED) {
+ /*
+ * Reopen the netlink socket and refresh the link
+ * status in case the fault was triggered by a missed
+ * netlink message (ENOBUFS).
+ */
+ port_rtnl_initialize(p);
}
}
--
2.43.0
@@ -0,0 +1,58 @@
From 3dece8ac8b324e702bf54875b53aee1eb49340e0 Mon Sep 17 00:00:00 2001
From: "Miroslav Lichvar (via linuxptp-devel Mailing List)"
<linuxptp-devel@lists.nwtime.org>
Date: Thu, 31 Jul 2025 11:35:46 +0200
Subject: [PATCH 10/13] uds: Copy ownership of server socket in pmc clients.
Organization: Wires
ptp4l sending a response to a pmc client needs to have permissions to
write to the client's UNIX domain socket. If ptp4l runs under a non-root
user, it cannot write to sockets bound by the pmc client if it did that
as root.
After binding the client socket, change its owner to the owner of the
server socket, so it can send the client a response.
Signed-off-by: Miroslav Lichvar <mlichvar@redhat.com>
Reviewed-by: Jacob Keller <jacob.e.keller@intel.com>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
uds.c | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/uds.c b/uds.c
index 4ddee7b..ce7e92d 100644
--- a/uds.c
+++ b/uds.c
@@ -60,6 +60,7 @@ static int uds_open(struct transport *t, struct interface *iface, struct fdarray
const char* file_mode_cfg;
struct sockaddr_un sa;
mode_t file_mode;
+ struct stat st;
int fd, err;
fd = socket(AF_LOCAL, SOCK_DGRAM, 0);
@@ -97,6 +98,20 @@ static int uds_open(struct transport *t, struct interface *iface, struct fdarray
uds->address.len = sizeof(sa);
chmod(name, file_mode);
+
+ /*
+ * In the client, copy the ownership of the server's socket if it runs
+ * under a non-root user to allow it to send a response to the client
+ * running under root. Avoid following a symlink if the socket is
+ * replaced (e.g. by compromised ptp4l process).
+ */
+ if (uds_path[0] != '\0') {
+ if (!lstat(uds_path, &st) && (st.st_uid || st.st_gid) &&
+ lchown(name, st.st_uid, st.st_gid)) {
+ pr_err("uds: failed to change socket ownership: %m");
+ }
+ }
+
fda->fd[FD_EVENT] = -1;
fda->fd[FD_GENERAL] = fd;
return 0;
--
2.43.0
@@ -0,0 +1,56 @@
From 6bfc1de21b64ac17145b119cb636d1c1b3d90a75 Mon Sep 17 00:00:00 2001
From: "Miroslav Lichvar (via linuxptp-devel Mailing List)"
<linuxptp-devel@lists.nwtime.org>
Date: Thu, 31 Jul 2025 11:35:47 +0200
Subject: [PATCH 11/13] uds: Don't call chmod() on client socket.
Organization: Wires
The pmc clients should not need to modify the permissions of their
socket (following the uds_file_mode setting), they can rely on their
umask.
Make the chmod() call on the bound socket only in the server.
This removes a race condition between the bind() and chmod() calls that
could potentially be exploited by ptp4l running under a non-root user.
It could replace the socket with a symlink in order to make the client
running under root to change the mode of a different file.
Signed-off-by: Miroslav Lichvar <mlichvar@redhat.com>
Reviewed-by: Jacob Keller <jacob.e.keller@intel.com>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
uds.c | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/uds.c b/uds.c
index ce7e92d..d74b7a8 100644
--- a/uds.c
+++ b/uds.c
@@ -97,19 +97,20 @@ static int uds_open(struct transport *t, struct interface *iface, struct fdarray
uds->address.sun = sa;
uds->address.len = sizeof(sa);
- chmod(name, file_mode);
-
/*
* In the client, copy the ownership of the server's socket if it runs
* under a non-root user to allow it to send a response to the client
* running under root. Avoid following a symlink if the socket is
- * replaced (e.g. by compromised ptp4l process).
+ * replaced (e.g. by compromised ptp4l process). The server just sets
+ * the permissions on its socket per configuration.
*/
if (uds_path[0] != '\0') {
if (!lstat(uds_path, &st) && (st.st_uid || st.st_gid) &&
lchown(name, st.st_uid, st.st_gid)) {
pr_err("uds: failed to change socket ownership: %m");
}
+ } else {
+ chmod(name, file_mode);
}
fda->fd[FD_EVENT] = -1;
--
2.43.0
@@ -0,0 +1,82 @@
From 5a192e152f4d19ff5899960154f1e14bbd0c6bd7 Mon Sep 17 00:00:00 2001
From: "Maxime Chevallier (via linuxptp-devel Mailing List)"
<linuxptp-devel@lists.nwtime.org>
Date: Wed, 4 Feb 2026 10:16:11 +0100
Subject: [PATCH 12/13] port: Allow mixing wildcard identities and exact
identities in messages
Organization: Wires
A Port Identity is made of a Clock Identity and a Port Number. Each of
these fields allow wildcards values.
The current implementation checks if either both these fields contain an
exact value, or if both contains a wildcard.
Make so that we check for wildcards on each field independently. To
avoid hard to read comparisons, introduce helper functions to compare
each field in PortIdentity.
Signed-off-by: Maxime Chevallier <maxime.chevallier@bootlin.com>
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
port_signaling.c | 6 ++++--
util.h | 26 ++++++++++++++++++++++++++
2 files changed, 30 insertions(+), 2 deletions(-)
diff --git a/port_signaling.c b/port_signaling.c
index cf28756..b34ebe9 100644
--- a/port_signaling.c
+++ b/port_signaling.c
@@ -151,8 +151,10 @@ int process_signaling(struct port *p, struct ptp_message *m)
}
/* Ignore signaling messages not addressed to this port. */
- if (!pid_eq(&m->signaling.targetPortIdentity, &p->portIdentity) &&
- !pid_eq(&m->signaling.targetPortIdentity, &wildcard_pid)) {
+ if ((!pid_cid_eq(&m->signaling.targetPortIdentity, &p->portIdentity) &&
+ !pid_cid_eq(&m->signaling.targetPortIdentity, &wildcard_pid)) ||
+ (!pid_pn_eq(&m->signaling.targetPortIdentity, &p->portIdentity) &&
+ !pid_pn_eq(&m->signaling.targetPortIdentity, &wildcard_pid))) {
return 0;
}
diff --git a/util.h b/util.h
index b228745..7552353 100644
--- a/util.h
+++ b/util.h
@@ -158,6 +158,32 @@ static inline int pid_eq(const struct PortIdentity *a,
return memcmp(a, b, sizeof(*a)) == 0;
}
+/**
+ * Compare two port identities for PortIdentity.clockIdentity equality.
+ *
+ * @param a First port identity.
+ * @param b Second port identity.
+ * @return 1 if identities are equal, 0 otherwise.
+ */
+static inline int pid_cid_eq(const struct PortIdentity *a,
+ const struct PortIdentity *b)
+{
+ return cid_eq(&a->clockIdentity, &b->clockIdentity);
+}
+
+/**
+ * Compare two port identities for PortIdentity.portNumber equality.
+ *
+ * @param a First port identity.
+ * @param b Second port identity.
+ * @return 1 if identities are equal, 0 otherwise.
+ */
+static inline int pid_pn_eq(const struct PortIdentity *a,
+ const struct PortIdentity *b)
+{
+ return a->portNumber == b->portNumber;
+}
+
/**
* Convert a string containing a network address into binary form.
* @param type The network transport type of the address.
--
2.43.0
@@ -0,0 +1,386 @@
From 4fdb58ce4e052862f0dc7432d84b19febd5256a5 Mon Sep 17 00:00:00 2001
From: Joachim Wiberg <troglobit@gmail.com>
Date: Fri, 10 Apr 2026 14:33:56 +0200
Subject: [PATCH 13/13] Add pidfile support to ptp4l, phc2sys, and timemaster
Organization: Wires
Add pidfile.c derived from OpenBSD via libite. The pidfile() function
creates a PID file and registers an atexit() handler to clean it up on
normal exit.
For ptp4l and phc2sys the pidfile path is set via the 'pidfile' global
config option (or -u <path> on the ptp4l command line). For timemaster,
which uses its own config format, the path is given with -u <path>.
Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
---
config.c | 1 +
makefile | 12 ++---
phc2sys.c | 7 +++
pidfile.c | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++
pidfile.h | 10 ++++
ptp4l.c | 14 ++++-
timemaster.c | 19 +++++--
7 files changed, 194 insertions(+), 11 deletions(-)
create mode 100644 pidfile.c
create mode 100644 pidfile.h
diff --git a/config.c b/config.c
index d0bc32c..4b46542 100644
--- a/config.c
+++ b/config.c
@@ -341,6 +341,7 @@ struct config_item config_tab[] = {
PORT_ITEM_INT("power_profile.2011.networkTimeInaccuracy", 0xFFFFFFFF, -1, INT_MAX),
PORT_ITEM_INT("power_profile.2017.totalTimeInaccuracy", 0xFFFFFFFF, -1, INT_MAX),
PORT_ITEM_INT("power_profile.grandmasterID", 0, 0, 0xFFFF),
+ GLOB_ITEM_STR("pidfile", NULL),
GLOB_ITEM_INT("priority1", 128, 0, UINT8_MAX),
GLOB_ITEM_INT("priority2", 128, 0, UINT8_MAX),
GLOB_ITEM_STR("productDescription", ";;"),
diff --git a/makefile b/makefile
index 3c2406b..67622e1 100644
--- a/makefile
+++ b/makefile
@@ -31,9 +31,9 @@ TS2PHC = ts2phc.o lstab.o nmea.o serial.o sock.o ts2phc_generic_pps_source.o \
ts2phc_nmea_pps_source.o ts2phc_phc_pps_source.o ts2phc_pps_sink.o ts2phc_pps_source.o
OBJ = bmc.o clock.o clockadj.o clockcheck.o config.o designated_fsm.o \
e2e_tc.o fault.o $(FILTERS) fsm.o hash.o interface.o monitor.o msg.o phc.o \
- pmc_common.o port.o port_signaling.o pqueue.o print.o ptp4l.o p2p_tc.o rtnl.o \
- $(SECURITY) $(SERVOS) sk.o stats.o tc.o $(TRANSP) telecom.o tlv.o tsproc.o \
- unicast_client.o unicast_fsm.o unicast_service.o util.o version.o
+ pidfile.o pmc_common.o port.o port_signaling.o pqueue.o print.o ptp4l.o \
+ p2p_tc.o rtnl.o $(SECURITY) $(SERVOS) sk.o stats.o tc.o $(TRANSP) telecom.o \
+ tlv.o tsproc.o unicast_client.o unicast_fsm.o unicast_service.o util.o version.o
OBJECTS = $(OBJ) hwstamp_ctl.o nsm.o phc2sys.o phc_ctl.o pmc.o pmc_agent.o \
pmc_common.o sysoff.o timemaster.o $(TS2PHC) tz2alt.o
@@ -78,14 +78,14 @@ pmc: config.o hash.o interface.o msg.o phc.o pmc.o pmc_common.o print.o \
$(SECURITY) sk.o tlv.o $(TRANSP) util.o version.o
phc2sys: clockadj.o clockcheck.o config.o hash.o interface.o msg.o \
- phc.o phc2sys.o pmc_agent.o pmc_common.o print.o $(SECURITY) $(SERVOS) \
- sk.o stats.o sysoff.o tlv.o $(TRANSP) util.o version.o
+ phc.o phc2sys.o pidfile.o pmc_agent.o pmc_common.o print.o $(SECURITY) \
+ $(SERVOS) sk.o stats.o sysoff.o tlv.o $(TRANSP) util.o version.o
hwstamp_ctl: hwstamp_ctl.o version.o
phc_ctl: phc_ctl.o phc.o sk.o util.o clockadj.o sysoff.o print.o version.o
-timemaster: phc.o print.o rtnl.o sk.o timemaster.o util.o version.o
+timemaster: phc.o pidfile.o print.o rtnl.o sk.o timemaster.o util.o version.o
ts2phc: config.o clockadj.o hash.o interface.o msg.o phc.o pmc_agent.o \
pmc_common.o print.o $(SECURITY) $(SERVOS) sk.o $(TS2PHC) tlv.o transport.o \
diff --git a/phc2sys.c b/phc2sys.c
index 5962f6c..b272adf 100644
--- a/phc2sys.c
+++ b/phc2sys.c
@@ -56,6 +56,7 @@
#include "sysoff.h"
#include "tlv.h"
#include "uds.h"
+#include "pidfile.h"
#include "util.h"
#include "version.h"
@@ -1432,6 +1433,12 @@ int main(int argc, char *argv[])
print_set_syslog(config_get_int(cfg, NULL, "use_syslog"));
print_set_level(config_get_int(cfg, NULL, "logging_level"));
+ if (config_get_string(cfg, NULL, "pidfile") &&
+ pidfile(config_get_string(cfg, NULL, "pidfile"))) {
+ fprintf(stderr, "failed to create pidfile\n");
+ goto end;
+ }
+
settings.free_running = config_get_int(cfg, NULL, "free_running");
settings.servo_type = config_get_int(cfg, NULL, "clock_servo");
if (settings.free_running || settings.servo_type == CLOCK_SERVO_NTPSHM) {
diff --git a/pidfile.c b/pidfile.c
new file mode 100644
index 0000000..42121b0
--- /dev/null
+++ b/pidfile.c
@@ -0,0 +1,142 @@
+/* Updated by troglobit for libite/finit/uftpd projects 2016/07/04 */
+/* $OpenBSD: pidfile.c,v 1.11 2015/06/03 02:24:36 millert Exp $ */
+/* $NetBSD: pidfile.c,v 1.4 2001/02/19 22:43:42 cgd Exp $ */
+
+/*-
+ * Copyright (c) 1999 The NetBSD Foundation, Inc.
+ * All rights reserved.
+ *
+ * This code is derived from software contributed to The NetBSD Foundation
+ * by Jason R. Thorpe.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <errno.h>
+#include <paths.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "pidfile.h"
+
+static char *pidfile_path = NULL;
+static pid_t pidfile_pid = 0;
+
+static void pidfile_cleanup(void);
+
+static const char *pidfile_rundir = _PATH_VARRUN;
+extern char *__progname;
+
+/**
+ * Create or update mtime of process PID file.
+ * @param basename Program name, or NULL, may start with '/'
+ *
+ * If @p basename is NULL the implicit @a __progname variable from the
+ * C-library is used. If @p basename starts with '/' it is used as the
+ * absolute path to the PID file.
+ *
+ * @returns POSIX OK(0) on success, non-zero with errno set on error.
+ */
+int pidfile(const char *basename)
+{
+ int save_errno;
+ int atexit_already;
+ pid_t pid;
+ FILE *f;
+
+ if (basename == NULL)
+ basename = __progname;
+
+ pid = getpid();
+ atexit_already = 0;
+
+ if (pidfile_path != NULL) {
+ if (!access(pidfile_path, R_OK) && pid == pidfile_pid) {
+ utimensat(0, pidfile_path, NULL, 0);
+ return 0;
+ }
+ free(pidfile_path);
+ pidfile_path = NULL;
+ atexit_already = 1;
+ }
+
+ if (basename[0] != '/') {
+ size_t len = strlen(pidfile_rundir);
+ int slash = pidfile_rundir[len > 0 ? len - 1 : 0] != '/';
+
+ if (asprintf(&pidfile_path, "%s%s%s.pid",
+ pidfile_rundir, slash ? "/" : "", basename) == -1)
+ return -1;
+ } else {
+ if (asprintf(&pidfile_path, "%s", basename) == -1)
+ return -1;
+ }
+
+ if ((f = fopen(pidfile_path, "w")) == NULL) {
+ save_errno = errno;
+ free(pidfile_path);
+ pidfile_path = NULL;
+ errno = save_errno;
+ return -1;
+ }
+
+ if (fprintf(f, "%ld\n", (long)pid) <= 0 || fflush(f) != 0) {
+ save_errno = errno;
+ (void)fclose(f);
+ (void)unlink(pidfile_path);
+ free(pidfile_path);
+ pidfile_path = NULL;
+ errno = save_errno;
+ return -1;
+ }
+ (void)fclose(f);
+
+ if (atexit_already)
+ return 0;
+
+ pidfile_pid = pid;
+ if (atexit(pidfile_cleanup) < 0) {
+ save_errno = errno;
+ (void)unlink(pidfile_path);
+ free(pidfile_path);
+ pidfile_path = NULL;
+ pidfile_pid = 0;
+ errno = save_errno;
+ return -1;
+ }
+
+ return 0;
+}
+
+static void pidfile_cleanup(void)
+{
+ if (pidfile_path != NULL && pidfile_pid == getpid()) {
+ (void)unlink(pidfile_path);
+ free(pidfile_path);
+ pidfile_path = NULL;
+ }
+}
diff --git a/pidfile.h b/pidfile.h
new file mode 100644
index 0000000..7b68c78
--- /dev/null
+++ b/pidfile.h
@@ -0,0 +1,10 @@
+/**
+ * @file pidfile.h
+ * @brief PID file support, derived from OpenBSD via libite.
+ */
+#ifndef HAVE_PIDFILE_H
+#define HAVE_PIDFILE_H
+
+int pidfile(const char *basename);
+
+#endif
diff --git a/ptp4l.c b/ptp4l.c
index ac2ef96..12f5e20 100644
--- a/ptp4l.c
+++ b/ptp4l.c
@@ -34,6 +34,7 @@
#include "transport.h"
#include "udp6.h"
#include "uds.h"
+#include "pidfile.h"
#include "util.h"
#include "version.h"
@@ -63,6 +64,7 @@ static void usage(char *progname)
" -l [num] set the logging level to 'num'\n"
" -m print messages to stdout\n"
" -q do not print messages to the syslog\n"
+ " -u [file] write process ID to 'file'\n"
" -v prints the software version and exits\n"
" -h prints this message and exits\n"
"\n",
@@ -90,7 +92,7 @@ int main(int argc, char *argv[])
/* Process the command line arguments. */
progname = strrchr(argv[0], '/');
progname = progname ? 1+progname : argv[0];
- while (EOF != (c = getopt_long(argc, argv, "AEP246HSLf:i:p:sl:mqvh",
+ while (EOF != (c = getopt_long(argc, argv, "AEP246HSLf:i:p:sl:mqu:vh",
opts, &index))) {
switch (c) {
case 0:
@@ -163,6 +165,10 @@ int main(int argc, char *argv[])
case 'q':
config_set_int(cfg, "use_syslog", 0);
break;
+ case 'u':
+ if (config_set_string(cfg, "pidfile", optarg))
+ goto out;
+ break;
case 'v':
version_show(stdout);
return 0;
@@ -188,6 +194,12 @@ int main(int argc, char *argv[])
print_set_syslog(config_get_int(cfg, NULL, "use_syslog"));
print_set_level(config_get_int(cfg, NULL, "logging_level"));
+ if (config_get_string(cfg, NULL, "pidfile") &&
+ pidfile(config_get_string(cfg, NULL, "pidfile"))) {
+ fprintf(stderr, "failed to create pidfile\n");
+ goto out;
+ }
+
assume_two_step = config_get_int(cfg, NULL, "assume_two_step");
sk_check_fupsync = config_get_int(cfg, NULL, "check_fup_sync");
sk_tx_timeout = config_get_int(cfg, NULL, "tx_timestamp_timeout");
diff --git a/timemaster.c b/timemaster.c
index b367b2f..873083f 100644
--- a/timemaster.c
+++ b/timemaster.c
@@ -38,6 +38,7 @@
#include <sys/wait.h>
#include <unistd.h>
+#include "pidfile.h"
#include "print.h"
#include "rtnl.h"
#include "sk.h"
@@ -1531,6 +1532,7 @@ static void usage(char *progname)
"\nusage: %s [options] -f file\n\n"
" -f file specify path to configuration file\n"
" -n only print generated files and commands\n"
+ " -u file write process ID to 'file'\n"
" -l level set logging level (6)\n"
" -m print messages to stdout\n"
" -q do not print messages to syslog\n"
@@ -1543,7 +1545,7 @@ int main(int argc, char **argv)
{
struct timemaster_config *config;
struct script *script;
- char *progname, *config_path = NULL;
+ char *progname, *config_path = NULL, *pid_file = NULL;
int c, ret = 0, log_stdout = 0, log_syslog = 1, dry_run = 0;
progname = strrchr(argv[0], '/');
@@ -1553,7 +1555,7 @@ int main(int argc, char **argv)
print_set_verbose(1);
print_set_syslog(0);
- while (EOF != (c = getopt(argc, argv, "f:nl:mqvh"))) {
+ while (EOF != (c = getopt(argc, argv, "f:nu:l:mqvh"))) {
switch (c) {
case 'f':
config_path = optarg;
@@ -1561,6 +1563,9 @@ int main(int argc, char **argv)
case 'n':
dry_run = 1;
break;
+ case 'u':
+ pid_file = optarg;
+ break;
case 'l':
print_set_level(atoi(optarg));
break;
@@ -1599,10 +1604,16 @@ int main(int argc, char **argv)
print_set_verbose(log_stdout);
print_set_syslog(log_syslog);
- if (dry_run)
+ if (dry_run) {
script_print(script);
- else
+ } else {
+ if (pid_file && pidfile(pid_file)) {
+ pr_err("failed to create pidfile %s: %m", pid_file);
+ script_destroy(script);
+ return 1;
+ }
ret = script_run(script);
+ }
script_destroy(script);
--
2.43.0
+18
View File
@@ -711,6 +711,23 @@ def keystore(args: List[str]) -> None:
print("Usage: show keystore [symmetric <name> | asymmetric <name>]")
def ptp(args: List[str]) -> None:
data = get_json("/ieee1588-ptp-tt:ptp")
if not data:
print("PTP: no instances running.")
return
if RAW_OUTPUT:
print(json.dumps(data, indent=2))
return
# Optional: filter to a specific instance-index
if args and args[0].isdigit():
cli_pretty(data, "show-ptp", args[0])
else:
cli_pretty(data, "show-ptp")
def execute_command(command: str, args: List[str]):
command_mapping = {
'bfd': bfd,
@@ -725,6 +742,7 @@ def execute_command(command: str, args: List[str]):
'nacm': nacm,
'ntp': ntp,
'ospf': ospf,
'ptp': ptp,
'rip': rip,
'routes': routes,
'services': services,
+1
View File
@@ -49,6 +49,7 @@ confd_plugin_la_SOURCES = \
keystore.c \
system.c \
ntp.c \
ptp.c \
syslog.c \
factory-default.c \
routing.c \
+9
View File
@@ -511,6 +511,10 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod
if ((rc = ntp_change(session, config, diff, event, confd)))
goto free_diff;
/* ieee1588-ptp-tt */
if ((rc = ptp_change(session, config, diff, event, confd)))
goto free_diff;
/* infix-services */
if ((rc = services_change(session, config, diff, event, confd)))
goto free_diff;
@@ -706,6 +710,11 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
ERROR("Failed to subscribe to infix-meta");
goto err;
}
rc = subscribe_model("ieee1588-ptp-tt", &confd, 0);
if (rc) {
ERROR("Failed to subscribe to ieee1588-ptp-tt");
goto err;
}
rc = system_rpc_init(&confd);
if (rc)
+3
View File
@@ -271,4 +271,7 @@ int ntp_cand(sr_session_ctx_t *session, uint32_t sub_id, const char *module,
const char *path, sr_event_t event, unsigned request_id, void *priv);
int ntp_candidate_init(struct confd *confd);
/* ptp.c */
int ptp_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);
#endif /* CONFD_CORE_H_ */
+465
View File
@@ -0,0 +1,465 @@
/* SPDX-License-Identifier: BSD-3-Clause */
#include <dirent.h>
#include <stdbool.h>
#include <jansson.h>
#include <srx/common.h>
#include <srx/lyx.h>
#include <srx/srx_val.h>
#include "core.h"
#define XPATH_PTP_ "/ieee1588-ptp-tt:ptp"
#define PTP_CONF_DIR "/etc/linuxptp"
/*
* Map instance-type string to ptp4l clockType keyword.
* Returns NULL for oc/bc (no explicit clockType needed in [global]).
*/
static const char *instance_type_to_clock_type(const char *type)
{
if (!type)
return NULL;
if (!strcmp(type, "p2p-tc"))
return "P2P_TC";
if (!strcmp(type, "e2e-tc"))
return "E2E_TC";
return NULL;
}
/*
* Emit all protocol-mandatory [global] settings for the chosen profile.
* Returns true when the profile is ieee802-dot1as (802.1AS/gPTP), which
* the caller uses to suppress per-port delay_mechanism output — identical
* to the guard already used for Transparent Clock instances.
*
* ieee802-dot1as sets the full gPTP option set as required by the standard:
* transportSpecific, network_transport, delay_mechanism, multicast MACs,
* gmCapable, follow_up_info, assume_two_step, path_trace_enabled,
* and the tighter neighborPropDelayThresh.
*/
static bool emit_profile_globals(FILE *fp, const char *profile)
{
bool dot1as = profile && !strcmp(profile, "ieee802-dot1as");
if (dot1as) {
fprintf(fp, "transportSpecific 1\n");
fprintf(fp, "network_transport L2\n");
fprintf(fp, "delay_mechanism P2P\n");
fprintf(fp, "ptp_dst_mac 01:80:C2:00:00:0E\n");
fprintf(fp, "p2p_dst_mac 01:80:C2:00:00:0E\n");
fprintf(fp, "gmCapable 1\n");
fprintf(fp, "follow_up_info 1\n");
fprintf(fp, "assume_two_step 1\n");
fprintf(fp, "path_trace_enabled 1\n");
fprintf(fp, "neighborPropDelayThresh 800\n");
} else {
fprintf(fp, "transportSpecific 0\n");
}
return dot1as;
}
/*
* Return true if ifname has hardware TX timestamping capability according
* to the probed data in system.json (confd->root). If the interface is
* absent from system.json (virtual interface, QEMU tap, etc.) or the
* "hardware-transmit" capability string is missing, returns false.
*/
static bool iface_has_hw_timestamp(json_t *root, const char *ifname)
{
json_t *caps, *list, *entry;
size_t i;
if (!root || !ifname)
return false;
caps = json_object_get(json_object_get(
json_object_get(root, "interfaces"),
ifname), "ptp-capabilities");
if (!caps)
return false;
list = json_object_get(caps, "capabilities");
if (!json_is_array(list))
return false;
json_array_foreach(list, i, entry) {
const char *s = json_string_value(entry);
if (s && !strcmp(s, "hardware-transmit"))
return true;
}
return false;
}
/*
* Scan all ports of inst and determine whether to use hardware or software
* timestamping. Emits a syslog WARNING when falling back to software due
* to a mixed or software-only set of port interfaces.
*/
static const char *instance_time_stamping(struct lyd_node *inst, json_t *root)
{
struct lyd_node *port;
bool any_sw = false;
LYX_LIST_FOR_EACH(lyd_child(lydx_get_child(inst, "ports")), port, "port") {
const char *iface = lydx_get_cattr(port, "underlying-interface");
if (!iface)
continue;
if (iface_has_hw_timestamp(root, iface))
continue;
any_sw = true;
break;
}
if (any_sw) {
WARN("PTP instance has software-only timestamping port(s), "
"falling back to time_stamping software");
return "software";
}
return "hardware";
}
/*
* Write ptp4l config for one PTP instance.
* Config file: /etc/linuxptp/ptp4l-<idx>.conf+ (staging)
*
* ptp4l key config layout:
* [global] — instance-wide settings
* [eth0] — per-port interface sections, sorted by port-index
*/
static int write_instance_conf(struct lyd_node *inst, json_t *root)
{
const char *instance_type, *clock_type, *profile;
struct lyd_node *default_ds, *port, *port_ds, *servo;
bool tc, bc, dot1as;
char path[256];
const char *v;
uint16_t idx;
FILE *fp;
v = lydx_get_cattr(inst, "instance-index");
if (!v)
return SR_ERR_INVAL_ARG;
idx = (uint16_t)atoi(v);
snprintf(path, sizeof(path), PTP_CONF_DIR "/ptp4l-%u.conf+", idx);
fp = fopen(path, "w");
if (!fp) {
ERRNO("Failed creating %s", path);
return SR_ERR_SYS;
}
fprintf(fp, "# Generated by confd — do not edit\n\n");
fprintf(fp, "[global]\n");
default_ds = lydx_get_child(inst, "default-ds");
instance_type = lydx_get_cattr(default_ds, "instance-type");
profile = lydx_get_cattr(default_ds, "infix-ptp:profile");
clock_type = instance_type_to_clock_type(instance_type);
tc = (clock_type != NULL);
bc = instance_type && !strcmp(instance_type, "bc");
/* Unique UDS socket per instance — required for pmc with multiple instances */
fprintf(fp, "uds_address /var/run/ptp4l-%u\n", idx);
/* Timestamping mode: hardware if all ports support it, software otherwise */
fprintf(fp, "time_stamping %s\n", instance_time_stamping(inst, root));
/* Profile — sets transportSpecific and all protocol-mandatory options */
dot1as = emit_profile_globals(fp, profile);
/* domainNumber */
v = lydx_get_cattr(default_ds, "domain-number");
if (v)
fprintf(fp, "domainNumber %s\n", v);
/* Transparent Clock clock_type */
if (tc)
fprintf(fp, "clock_type %s\n", clock_type);
/*
* Multi-port instances (BC and TC) may span ports on different PHC
* devices when the ports belong to different switch chips (e.g. a
* three-chip board where each mv88e6xxx chip owns its own /dev/ptpN).
* boundary_clock_jbod silences ptp4l's startup PHC-mismatch check and
* lets each port use its own PHC. It is a no-op when all ports share
* the same PHC (single-chip board or software timestamping).
*
* NOTE: for BC on multi-chip hardware, ptp4l only disciplines the PHC
* of the active slave port; the other chips' PHCs will drift unless
* phc2sys(8) -a is also run. That is a separate service (TODO).
*/
if (tc || bc)
fprintf(fp, "boundary_clock_jbod 1\n");
/* priority1 / priority2 (not applicable for TC, but harmless) */
v = lydx_get_cattr(default_ds, "priority1");
if (v)
fprintf(fp, "priority1 %s\n", v);
v = lydx_get_cattr(default_ds, "priority2");
if (v)
fprintf(fp, "priority2 %s\n", v);
/* clientOnly (OC only) — inclusive replacement for slaveOnly in ptp4l 4.x */
v = lydx_get_cattr(default_ds, "time-receiver-only");
if (v && !strcmp(v, "true"))
fprintf(fp, "clientOnly 1\n");
/* maxStepsRemoved */
v = lydx_get_cattr(default_ds, "max-steps-removed");
if (v)
fprintf(fp, "maxStepsRemoved %s\n", v);
/* servo: step_threshold (0.0 = slew-only, never step) */
servo = lydx_get_child(inst, "servo");
if (servo) {
v = lydx_get_cattr(servo, "step-threshold");
if (v)
fprintf(fp, "step_threshold %s\n", v);
}
/*
* Transparent Clocks set delay_mechanism globally; ptp4l ignores
* per-port delay_mechanism for TCs. 802.1AS mandates P2P globally
* (already emitted by emit_profile_globals).
*/
if (tc) {
if (!strcmp(clock_type, "P2P_TC"))
fprintf(fp, "delay_mechanism P2P\n");
else
fprintf(fp, "delay_mechanism E2E\n");
}
fprintf(fp, "\n");
/* Per-port [interface] sections, sorted by port-index */
LYX_LIST_FOR_EACH(lyd_child(lydx_get_child(inst, "ports")), port, "port") {
const char *iface;
iface = lydx_get_cattr(port, "underlying-interface");
if (!iface)
continue;
port_ds = lydx_get_child(port, "port-ds");
if (!port_ds)
continue;
if (!lydx_is_enabled(port_ds, "port-enable"))
continue;
fprintf(fp, "[%s]\n", iface);
v = lydx_get_cattr(port_ds, "log-announce-interval");
if (v)
fprintf(fp, "logAnnounceInterval %s\n", v);
v = lydx_get_cattr(port_ds, "announce-receipt-timeout");
if (v)
fprintf(fp, "announceReceiptTimeout %s\n", v);
v = lydx_get_cattr(port_ds, "log-sync-interval");
if (v)
fprintf(fp, "logSyncInterval %s\n", v);
v = lydx_get_cattr(port_ds, "log-min-delay-req-interval");
if (v)
fprintf(fp, "logMinDelayReqInterval %s\n", v);
v = lydx_get_cattr(port_ds, "log-min-pdelay-req-interval");
if (v)
fprintf(fp, "logMinPdelayReqInterval %s\n", v);
/*
* delay_mechanism per port — only for OC/BC on ieee1588 profile.
* TC and 802.1AS both set it globally; ptp4l ignores per-port
* overrides in those cases.
*/
if (!tc && !dot1as) {
const char *dm = lydx_get_cattr(port_ds, "delay-mechanism");
if (dm) {
if (!strcmp(dm, "p2p"))
fprintf(fp, "delay_mechanism P2P\n");
else if (!strcmp(dm, "e2e"))
fprintf(fp, "delay_mechanism E2E\n");
}
}
v = lydx_get_cattr(port_ds, "delay-asymmetry");
if (v && strcmp(v, "0"))
fprintf(fp, "delayAsymmetry %s\n", v);
if (lydx_is_enabled(port_ds, "time-transmitter-only"))
fprintf(fp, "masterOnly 1\n");
fprintf(fp, "\n");
}
fclose(fp);
return SR_ERR_OK;
}
/*
* Remove staging config for one instance.
*/
static void remove_staging(uint16_t idx)
{
char path[256];
snprintf(path, sizeof(path), PTP_CONF_DIR "/ptp4l-%u.conf+", idx);
(void)remove(path);
}
/*
* Activate one instance: rename staging → live, enable finit service.
*/
static int activate_instance(uint16_t idx)
{
char staging[256], live[256];
snprintf(staging, sizeof(staging), PTP_CONF_DIR "/ptp4l-%u.conf+", idx);
snprintf(live, sizeof(live), PTP_CONF_DIR "/ptp4l-%u.conf", idx);
if (!fexist(staging)) {
(void)remove(live);
return SR_ERR_OK;
}
if (rename(staging, live)) {
ERRNO("Failed renaming %s → %s", staging, live);
return SR_ERR_SYS;
}
finit_enablef("ptp4l@%u", idx);
return finit_reloadf("ptp4l@%u", idx);
}
/*
* Deactivate (disable) one instance and remove its live config.
*/
static void deactivate_instance(uint16_t idx)
{
char live[256];
finit_disablef("ptp4l@%u", idx);
snprintf(live, sizeof(live), PTP_CONF_DIR "/ptp4l-%u.conf", idx);
(void)remove(live);
}
/*
* Disable any ptp4l@ services in finit enabled/ whose index is not in the
* currently configured set. Called from SR_EV_DONE after enabling active
* instances, to clean up stale services from a previous config.
*/
static void cleanup_stale_instances(struct lyd_node *config)
{
const struct dirent *ent;
struct lyd_node *inst;
int idx;
DIR *d;
d = opendir(FINIT_RCSD "/enabled");
if (!d)
return;
while ((ent = readdir(d))) {
bool found = false;
if (sscanf(ent->d_name, "ptp4l@%d.conf", &idx) != 1)
continue;
/* Is this index still configured? */
LYX_LIST_FOR_EACH(lydx_get_descendant(config, "ptp", "instances", "instance", NULL),
inst, "instance") {
const char *v = lydx_get_cattr(inst, "instance-index");
if (v && atoi(v) == idx) {
found = true;
break;
}
}
if (!found)
deactivate_instance((uint16_t)idx);
}
closedir(d);
}
static int change(sr_session_ctx_t *session, struct lyd_node *config,
struct lyd_node *diff, sr_event_t event, struct confd *confd)
{
struct lyd_node *instances, *inst;
int rc = SR_ERR_OK;
if (diff && !lydx_get_xpathf(diff, XPATH_PTP_))
return SR_ERR_OK;
switch (event) {
case SR_EV_ENABLED:
case SR_EV_CHANGE:
break;
case SR_EV_ABORT:
/* Remove any staging files */
instances = lydx_get_descendant(config, "ptp", "instances", "instance", NULL);
LYX_LIST_FOR_EACH(instances, inst, "instance") {
const char *v = lydx_get_cattr(inst, "instance-index");
if (v)
remove_staging((uint16_t)atoi(v));
}
return SR_ERR_OK;
case SR_EV_DONE:
/* Activate all configured instances */
instances = lydx_get_descendant(config, "ptp", "instances", "instance", NULL);
LYX_LIST_FOR_EACH(instances, inst, "instance") {
const char *v = lydx_get_cattr(inst, "instance-index");
if (!v)
continue;
if ((rc = activate_instance((uint16_t)atoi(v))))
return rc;
}
/* Disable stale services not in current config */
cleanup_stale_instances(config);
if (!instances)
return SR_ERR_OK;
return SR_ERR_OK;
default:
return SR_ERR_OK;
}
/* SR_EV_ENABLED / SR_EV_CHANGE — generate staging configs */
instances = lydx_get_descendant(config, "ptp", "instances", "instance", NULL);
if (!instances)
return SR_ERR_OK;
if (mkdir(PTP_CONF_DIR, 0755) && errno != EEXIST) {
ERRNO("Failed creating " PTP_CONF_DIR);
return SR_ERR_SYS;
}
LYX_LIST_FOR_EACH(instances, inst, "instance") {
rc = write_instance_conf(inst, confd->root);
if (rc)
return rc;
}
return SR_ERR_OK;
}
int ptp_change(sr_session_ctx_t *session, struct lyd_node *config,
struct lyd_node *diff, sr_event_t event, struct confd *confd)
{
return change(session, config, diff, event, confd);
}
+4 -1
View File
@@ -47,10 +47,13 @@ MODULES=(
"ieee802-ethernet-interface@2019-06-21.yang"
"infix-ethernet-interface@2024-02-27.yang"
"infix-factory-default@2023-06-28.yang"
"infix-interfaces@2025-11-06.yang -e vlan-filtering"
"infix-interfaces@2026-04-09.yang -e vlan-filtering"
"ietf-crypto-types -e cleartext-symmetric-keys"
"infix-crypto-types@2026-02-14.yang"
"ietf-keystore -e symmetric-keys"
"infix-ntp@2026-03-09.yang"
"infix-keystore@2025-12-17.yang"
"ieee1588-ptp-tt@2023-08-14.yang -e timestamp-correction"
"ieee802-dot1as-gptp@2025-12-10.yang"
"infix-ptp@2026-04-07.yang"
)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+102
View File
@@ -0,0 +1,102 @@
submodule infix-if-ptp {
yang-version 1.1;
belongs-to infix-interfaces {
prefix infix-if;
}
import ietf-interfaces {
prefix if;
}
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "PTP timestamping capabilities for ietf-interfaces.";
revision 2026-04-09 {
description "Initial revision.";
reference "internal";
}
/*
* Data Nodes
*/
augment "/if:interfaces/if:interface" {
description
"PTP timestamping capabilities reported by the network driver.
Data is probed at boot via ethtool --json -T and is read-only.";
container ptp-capabilities {
config false;
description
"PTP hardware and software timestamping capabilities of this
interface, as reported by the driver via ethtool -T. Absent
on virtual interfaces (bridges, VLANs, etc.) that have no
underlying physical device.";
leaf-list capabilities {
type enumeration {
enum software-transmit {
description "Software TX timestamping supported.";
}
enum software-receive {
description "Software RX timestamping supported.";
}
enum software-system-clock {
description "System clock can be used for SW timestamping.";
}
enum hardware-transmit {
description "Hardware TX timestamping supported.";
}
enum hardware-receive {
description "Hardware RX timestamping supported.";
}
enum hardware-raw-clock {
description "Raw hardware clock (PHC) exposed to userspace.";
}
}
description
"Set of timestamping capability flags reported by the driver.
The presence of hardware-transmit indicates that ptp4l can
use hardware timestamping on this interface.";
}
leaf phc-index {
type uint32;
description
"PTP Hardware Clock device index (e.g. 0 for /dev/ptp0).
Absent when the interface has no associated PHC.";
}
leaf-list tx-types {
type string;
description
"Hardware TX timestamp types supported (e.g. 'off', 'on',
'onestep-sync'). Empty when hardware-transmit is not in
the capabilities set.";
}
leaf-list rx-filters {
type string;
description
"Hardware RX timestamp filter modes supported (e.g.
'ptpv2-l2-event', 'ptpv2-l4-sync'). Empty when
hardware-receive is not in the capabilities set.";
}
leaf hwtstamp-provider-index {
type uint32;
description
"Hardware timestamp provider index. Present only on kernels
and drivers that expose per-provider timestamping (Linux 6.x+).";
}
leaf hwtstamp-provider-qualifier {
type string;
description
"Human-readable quality descriptor for the hardware timestamp
provider, e.g. 'Precise (IEEE 1588 quality)'.";
}
}
}
}
+1
View File
@@ -0,0 +1 @@
infix-if-ptp.yang
@@ -35,11 +35,17 @@ module infix-interfaces {
include infix-if-vxlan;
include infix-if-wifi;
include infix-if-wireguard;
include infix-if-ptp;
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Linux bridge and lag extensions for ietf-interfaces.";
revision 2026-04-09 {
description "Add ptp-capabilities submodule for per-interface PTP timestamping info.";
reference "internal";
}
revision 2025-11-06 {
description "Use new tunnel-common grouping for local, remote, ttl, and tos.";
reference "internal";
+291
View File
@@ -0,0 +1,291 @@
module infix-ptp {
yang-version 1.1;
namespace "urn:infix:ptp:ns:yang:1.0";
prefix infix-ptp;
import ieee1588-ptp-tt {
prefix ptp-tt;
}
import ieee802-dot1as-gptp {
prefix dot1as-gptp;
}
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Augments and deviations for IEEE 1588-2019 and
IEEE 802.1AS-2020 PTP support.
Profile selection via the profile leaf covers all
protocol-mandatory settings for each profile. The
standard sdo-id leaf is deviated not-supported; the
profile leaf is the authoritative selector.
Transparent Clock support uses the modern instance-type
approach (p2p-tc / e2e-tc) from IEEE 1588-2019. The
deprecated IEEE 1588-2008 transparent-clock-default-ds
and transparent-clock-ports containers are deviated
not-supported.";
revision 2026-04-07 {
description "Initial revision.";
reference "internal";
}
/*
* Augments
*/
augment "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance"
+ "/ptp-tt:default-ds" {
description
"Profile selection for this PTP instance.
The profile leaf selects the PTP profile and applies all
protocol-mandatory settings for that profile. The standard
sdo-id leaf is deviated not-supported; use profile instead.";
leaf profile {
type enumeration {
enum ieee1588 {
value 0;
description
"IEEE 1588-2019 default profile. Uses UDP/IPv4 transport
and E2E delay measurement by default. Network transport
and delay mechanism are user-configurable per port.";
}
enum ieee802-dot1as {
value 1;
description
"IEEE 802.1AS-2020 gPTP profile. Applies all
protocol-mandatory settings: IEEE 802.3 (Layer 2)
transport, P2P delay measurement, and the 802.1AS
multicast group address. Also enables path trace,
follow-up information, and neighbor propagation delay
thresholds as required by the standard.
User-configurable: priority1, priority2, domain-number,
time-receiver-only, and timer interval leaves.";
}
}
default ieee1588;
description
"PTP profile for this instance. Selects the complete set of
protocol-mandatory settings for the chosen profile.
The combination of domain-number and profile must be unique
across all PTP instances on this node.";
}
}
augment "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance" {
description
"Clock servo parameters for this PTP instance.";
container servo {
description
"Clock servo tuning parameters.";
leaf step-threshold {
type decimal64 {
fraction-digits 9;
}
units "seconds";
default "0.0";
description
"Maximum offset from the time transmitter that the servo
corrects by slewing rather than stepping. When the measured
offset exceeds this threshold the clock is stepped abruptly
to the correct time; below the threshold the servo disciplines
the clock by frequency adjustment only.
The value 0.0 (default) disables stepping: the servo always
slews, which guarantees a monotonic clock at the cost of
potentially very slow convergence when starting from a large
initial offset.
Setting a non-zero value (for example 0.1 for 100 ms) allows
the servo to step the clock on first lock, achieving fast
initial convergence while keeping the clock monotonic once
it has locked.";
}
}
}
/*
* Deviations from ieee1588-ptp-tt
*/
/*
* /ptp/instances/instance/default-ds
*/
deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance"
+ "/ptp-tt:default-ds/ptp-tt:sdo-id" {
deviate not-supported;
description
"Only the upper 4-bit majorSdoId field is configurable.
Use the profile leaf to select the correct majorSdoId value.";
}
deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance"
+ "/ptp-tt:default-ds/ptp-tt:current-time" {
deviate not-supported;
description
"Setting PTP instance time via YANG is not supported.
The current PTP time is only observable via pmc and is
presented in operational data.";
}
deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance"
+ "/ptp-tt:default-ds/ptp-tt:external-port-config-enable" {
deviate not-supported;
description
"The external-port-config feature is not supported.";
}
/*
* /ptp/instances/instance/description-ds
*/
deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance"
+ "/ptp-tt:description-ds" {
deviate not-supported;
description
"The description data set is not exposed by pmc and is
not supported in this implementation.";
}
/*
* /ptp/instances/instance/fault-log-ds
* (feature-gated; not enabled)
*/
deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance"
+ "/ptp-tt:fault-log-ds" {
deviate not-supported;
description
"Structured fault log is not supported. ptp4l does not
expose a fault log via pmc. Faults are observable in
syslog (tagged ptp4l).";
}
/*
* /ptp/instances/instance/path-trace-ds
* (feature-gated; not enabled)
*/
deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance"
+ "/ptp-tt:path-trace-ds" {
deviate not-supported;
description
"Path trace mechanism is not supported in this implementation.";
}
/*
* /ptp/instances/instance/alternate-timescale-ds
* (feature-gated; not enabled)
*/
deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance"
+ "/ptp-tt:alternate-timescale-ds" {
deviate not-supported;
description
"Alternate timescale mechanism is not supported.";
}
/*
* /ptp/instances/instance/ports/port/port-ds
*/
deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance"
+ "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds"
+ "/ptp-tt:peer-mean-path-delay" {
deviate not-supported;
description
"Deprecated in IEEE 1588-2019; superseded by mean-link-delay.
Not supported.";
}
deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance"
+ "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds"
+ "/ptp-tt:version-number" {
deviate not-supported;
description
"PTP version is determined by ptp4l at runtime and is not
user-configurable.";
}
deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance"
+ "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds"
+ "/ptp-tt:minor-version-number" {
deviate not-supported;
description
"PTP minor version is determined by ptp4l at runtime and is
not user-configurable.";
}
deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance"
+ "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds"
+ "/ptp-tt:port-enable" {
deviate add {
default "true";
}
description
"Ports are enabled by default.";
}
/*
* /ptp/transparent-clock-default-ds (deprecated IEEE 1588-2008)
*/
deviation "/ptp-tt:ptp/ptp-tt:transparent-clock-default-ds" {
deviate not-supported;
description
"Deprecated IEEE 1588-2008 container. Use the modern
instance-type = p2p-tc or e2e-tc approach in
/ptp/instances/instance/default-ds/instance-type instead.";
}
/*
* /ptp/transparent-clock-ports (deprecated IEEE 1588-2008)
*/
deviation "/ptp-tt:ptp/ptp-tt:transparent-clock-ports" {
deviate not-supported;
description
"Deprecated IEEE 1588-2008 container. Transparent Clock
ports are managed via /ptp/instances/instance/ports instead.";
}
/*
* /ptp/common-services (CMLDS Phase 2)
*/
deviation "/ptp-tt:ptp/ptp-tt:common-services" {
deviate not-supported;
description
"CMLDS (Common Mean Link Delay Service) is not supported yet.";
}
/*
* Deviations from ieee802-dot1as-gptp
*/
deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance"
+ "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds"
+ "/dot1as-gptp:nup" {
deviate not-supported;
description
"EPON upstream refraction index — not applicable to
Ethernet/TSN deployments.";
}
deviation "/ptp-tt:ptp/ptp-tt:instances/ptp-tt:instance"
+ "/ptp-tt:ports/ptp-tt:port/ptp-tt:port-ds"
+ "/dot1as-gptp:ndown" {
deviate not-supported;
description
"EPON downstream refraction index — not applicable to
Ethernet/TSN deployments.";
}
}
+1
View File
@@ -0,0 +1 @@
infix-ptp.yang
+7
View File
@@ -422,6 +422,13 @@ echo "Public: $pub"
<ACTION sym="script" in="tty" out="tty" interrupt="true">show nacm</ACTION>
</COMMAND>
<COMMAND name="ptp" help="Show PTP (IEEE 1588/802.1AS) status">
<SWITCH name="optional" min="0" max="1">
<PARAM name="instance" ptype="/INT" help="Show specific PTP instance (instance-index)"/>
</SWITCH>
<ACTION sym="script">show ptp $KLISH_PARAM_instance</ACTION>
</COMMAND>
<COMMAND name="ntp" help="Show NTP status">
<SWITCH name="subcommands" min="0">
<COMMAND name="source" help="Show NTP source(s)">
+202
View File
@@ -5636,6 +5636,203 @@ def show_bfd(json_data):
show_bfd_peers_brief(json_data)
def _ptp_ns(yang_val):
"""Convert YANG time-interval (ns × 2^16, stored as str) to integer nanoseconds."""
try:
return int(yang_val) // 65536
except (TypeError, ValueError):
return None
def _ptp_strip(identity):
"""Strip YANG module prefix from identityref value.
'ieee1588-ptp-tt:cc-default''cc-default'
"""
if identity and ':' in identity:
return identity.split(':', 1)[1]
return identity or ''
_PTP_INSTANCE_TYPE_NAMES = {
"oc": "Ordinary Clock",
"bc": "Boundary Clock",
"p2p-tc": "P2P Transparent Clock",
"e2e-tc": "E2E Transparent Clock",
}
_PTP_PORT_STATE_COLOR = {
"time-transmitter": Decore.green,
"time-receiver": Decore.green,
"pre-time-transmitter": Decore.yellow,
"uncalibrated": Decore.yellow,
"listening": Decore.yellow,
"passive": lambda x: x,
"faulty": Decore.red,
"disabled": Decore.red,
"initializing": lambda x: x,
}
def show_ptp(json_data, instance_index=None):
"""Show PTP instance status."""
ptp = json_data.get("ieee1588-ptp-tt:ptp", {})
instances = ptp.get("instances", {}).get("instance", [])
if not instances:
print("PTP: no instances configured.")
return
for inst in instances:
idx = inst.get("instance-index", "?")
if instance_index is not None and str(idx) != str(instance_index):
continue
dds = inst.get("default-ds", {})
cds = inst.get("current-ds", {})
pds = inst.get("parent-ds", {})
tpds = inst.get("time-properties-ds", {})
ports = inst.get("ports", {}).get("port", [])
itype = _PTP_INSTANCE_TYPE_NAMES.get(dds.get("instance-type", "oc"), "Unknown")
domain = dds.get("domain-number", 0)
clock_id = dds.get("clock-identity", "?")
# ── Header ────────────────────────────────────────────────────────────
header = f"PTP Instance {idx}"
subtitle = f"{itype} · domain {domain}"
pad_len = max(1, 40 - len(header))
pad = " " * pad_len
rule_w = max(68, len(header) + pad_len + len(subtitle))
print(f"{Decore.bold(header)}{pad}{subtitle}")
print("" * rule_w)
# ── Clock / GM identity ───────────────────────────────────────────────
W = 24
gm_id = pds.get("grandmaster-identity", "")
print(f" {'Clock identity':<{W}}: {clock_id}")
if gm_id and gm_id != clock_id:
print(f" {'Grandmaster':<{W}}: {gm_id}")
else:
print(f" {'Grandmaster':<{W}}: (self)")
# ── Priorities ────────────────────────────────────────────────────────
p1, p2 = dds.get("priority1", "?"), dds.get("priority2", "?")
print(f" {'Priority1/Priority2':<{W}}: {p1} / {p2}")
if gm_id and gm_id != clock_id:
gp1 = pds.get("grandmaster-priority1", "?")
gp2 = pds.get("grandmaster-priority2", "?")
print(f" {'GM Priority1/Priority2':<{W}}: {gp1} / {gp2}")
# ── Clock quality ─────────────────────────────────────────────────────
cq = dds.get("clock-quality", {})
cc = _ptp_strip(cq.get("clock-class", ""))
if cc:
print(f" {'Clock class':<{W}}: {cc}")
if gm_id and gm_id != clock_id:
gcq = pds.get("grandmaster-clock-quality", {})
gcc = _ptp_strip(gcq.get("clock-class", ""))
if gcc and gcc != cc:
print(f" {'GM clock class':<{W}}: {gcc}")
# ── Time source ───────────────────────────────────────────────────────
ts = _ptp_strip(tpds.get("time-source", ""))
if ts:
print(f" {'Time source':<{W}}: {ts}")
if dds.get("time-receiver-only"):
print(f" {'Mode':<{W}}: time-receiver only")
# ── Time properties ───────────────────────────────────────────────────
ptp_ts = "yes" if tpds.get("ptp-timescale") else "no"
t_trace = "yes" if tpds.get("time-traceable") else "no"
f_trace = "yes" if tpds.get("frequency-traceable") else "no"
utc_off = tpds.get("current-utc-offset")
utc_str = f"{utc_off} s" if utc_off is not None else "N/A"
print(f" {'PTP timescale':<{W}}: {ptp_ts}")
print(f" {'UTC offset':<{W}}: {utc_str}")
print(f" {'Time traceable':<{W}}: {t_trace}")
print(f" {'Freq. traceable':<{W}}: {f_trace}")
# ── Sync status ───────────────────────────────────────────────────────
offset = _ptp_ns(cds.get("offset-from-time-transmitter"))
delay = _ptp_ns(cds.get("mean-delay"))
steps = cds.get("steps-removed")
if offset is not None:
print(f" {'Offset from GM':<{W}}: {offset} ns")
if delay is not None:
print(f" {'Mean path delay':<{W}}: {delay} ns")
if steps is not None:
print(f" {'Steps removed':<{W}}: {steps}")
# ── Ports ─────────────────────────────────────────────────────────────
if ports:
print()
Decore.title("Ports", width=rule_w)
port_table = SimpleTable([
Column("PORT", align='right'),
Column("INTERFACE", flexible=True),
Column("STATE", flexible=True),
Column("DELAY"),
Column("LINK DELAY (ns)", align='right'),
])
stats_table = SimpleTable([
Column("PORT", align='right'),
Column("INTERFACE", flexible=True),
Column("SYNC \u25bc", align='right'),
Column("SYNC \u25b2", align='right'),
Column("ANN \u25bc", align='right'),
Column("ANN \u25b2", align='right'),
Column("PD \u25bc", align='right'),
Column("PD \u25b2", align='right'),
])
has_stats = False
for port in ports:
pidx = port.get("port-index", "?")
pds_ = port.get("port-ds", {})
iface = port.get("underlying-interface",
pds_.get("port-identity", {}).get("clock-identity", "?"))
state_raw = pds_.get("port-state", "?")
color_fn = _PTP_PORT_STATE_COLOR.get(state_raw, lambda x: x)
state_str = color_fn(state_raw)
dm = (pds_.get("delay-mechanism") or "?").upper()
mld = _ptp_ns(pds_.get("mean-link-delay"))
mld_str = str(mld) if mld is not None else ""
port_table.row(str(pidx), iface, state_str, dm, mld_str)
st = port.get("ieee802-dot1as-gptp:port-statistics-ds", {})
if st:
has_stats = True
stats_table.row(
str(pidx), iface,
str(st.get("rx-sync-count", 0)),
str(st.get("tx-sync-count", 0)),
str(st.get("rx-announce-count", 0)),
str(st.get("tx-announce-count", 0)),
str(st.get("rx-pdelay-req-count", 0)),
str(st.get("tx-pdelay-req-count", 0)),
)
port_table.adjust_padding(rule_w)
port_table.print()
if has_stats:
print()
Decore.title("Message Statistics (\u25bc\u202frx \u25b2\u202ftx)", width=rule_w)
stats_table.adjust_padding(rule_w)
stats_table.print()
print()
def main():
global UNIT_TEST
@@ -5695,6 +5892,9 @@ def main():
ks_parser.add_argument('-t', '--type', help='Key type (symmetric or asymmetric)')
ks_parser.add_argument('-n', '--name', help='Key name')
subparsers.add_parser('show-ptp', help='Show PTP instance status') \
.add_argument('instance', nargs='?', help='Instance index (optional)')
subparsers.add_parser('show-ntp', help='Show NTP status') \
.add_argument('-a', '--address', help='Show details for specific address')
subparsers.add_parser('show-ntp-tracking', help='Show NTP tracking status')
@@ -5768,6 +5968,8 @@ def main():
show_nacm_user(json_data)
elif args.command == "show-keystore":
show_keystore(json_data, getattr(args, 'type', None), args.name)
elif args.command == "show-ptp":
show_ptp(json_data, getattr(args, 'instance', None))
elif args.command == "show-ntp":
show_ntp(json_data, args.address)
elif args.command == "show-ntp-tracking":
+3
View File
@@ -123,6 +123,9 @@ def main():
elif model == 'ietf-bfd-ip-sh':
from . import ietf_bfd_ip_sh
yang_data = ietf_bfd_ip_sh.operational()
elif model == 'ieee1588-ptp-tt':
from . import ieee1588_ptp
yang_data = ieee1588_ptp.operational()
else:
common.LOG.warning("Unsupported model %s", model)
sys.exit(1)
+624
View File
@@ -0,0 +1,624 @@
"""Operational data for ieee1588-ptp-tt (and ieee802-dot1as-gptp).
Queries each running ptp4l instance via pmc and maps the output to the
YANG model structure. One ptp4l process runs per instance-index, with
its config at /etc/linuxptp/ptp4l-<idx>.conf and its UDS socket at
/var/run/ptp4l-<idx>.
"""
import glob
import os
import re
from .common import insert, LOG
from .host import HOST
# ---------------------------------------------------------------------------
# pmc helpers
# ---------------------------------------------------------------------------
def _pmc_get(conf, command):
"""Run 'pmc -b 0 -f <conf> GET <command>' and return parsed key→value dict.
pmc output looks like:
\t\t<key> <value>
Blank lines and lines not starting with whitespace are ignored.
Multiple response blocks (one per port for PORT_DATA_SET) each get
their own dict; returns a list of dicts in that case.
"""
lines = HOST.run_multiline(
["pmc", "-u", "-b", "0", "-f", conf, f"GET {command}"], default=[])
blocks = []
current = {}
for line in lines:
stripped = line.strip()
if not stripped or stripped.startswith("sending:") or \
"RESPONSE MANAGEMENT" in stripped or \
"SIGNALING" in stripped:
if current:
blocks.append(current)
current = {}
continue
m = re.match(r'^\s+(\S+)\s+(.+)$', line)
if m:
current[m.group(1)] = m.group(2).strip()
if current:
blocks.append(current)
return blocks
def _pmc_get_one(conf, command):
"""Like _pmc_get but return only the first (or only) block."""
blocks = _pmc_get(conf, command)
return blocks[0] if blocks else {}
# ---------------------------------------------------------------------------
# clockIdentity formatting
# ---------------------------------------------------------------------------
def _fmt_clock_identity(raw):
"""Convert pmc clockIdentity 'aabbcc.fffe.ddeeff' to YANG format 'AA-BB-CC-FF-FE-DD-EE-FF'.
The YANG typedef clock-identity requires the pattern [0-9A-F]{2}(-[0-9A-F]{2}){7}.
pmc outputs in its own dotted notation e.g. '005182.fffe.112202'.
"""
raw = raw.replace(".", "").replace("-", "").replace(":", "").upper()
if len(raw) == 16:
return "-".join(raw[i:i+2] for i in range(0, 16, 2))
return raw
def _fmt_port_identity(raw):
"""Convert 'aabbccfffe001122-1' to dict {clock-identity, port-number}."""
parts = raw.rsplit("-", 1)
cid = _fmt_clock_identity(parts[0]) if parts else raw
pnum = int(parts[1]) if len(parts) == 2 else 0
return {"clock-identity": cid, "port-number": pnum}
# ---------------------------------------------------------------------------
# clockAccuracy identity mapping
# ---------------------------------------------------------------------------
# Map clockClass decimal values to ieee1588-ptp-tt identity names (identityref, not uint8).
_CLOCK_CLASS_MAP = {
6: "ieee1588-ptp-tt:cc-primary-sync",
7: "ieee1588-ptp-tt:cc-primary-sync-lost",
13: "ieee1588-ptp-tt:cc-application-specific-sync",
14: "ieee1588-ptp-tt:cc-application-specific-sync-lost",
52: "ieee1588-ptp-tt:cc-primary-sync-alternative-a",
58: "ieee1588-ptp-tt:cc-application-specific-alternative-a",
187: "ieee1588-ptp-tt:cc-primary-sync-alternative-b",
193: "ieee1588-ptp-tt:cc-application-specific-alternative-b",
248: "ieee1588-ptp-tt:cc-default",
255: "ieee1588-ptp-tt:cc-time-receiver-only",
}
def _clock_class_identity(raw):
"""Return the YANG identity string for a pmc clockClass decimal value, or None."""
try:
return _CLOCK_CLASS_MAP.get(int(raw))
except (ValueError, TypeError):
return None
# Map clockAccuracy hex values to ieee1588-ptp-tt identity names (identityref, not uint8).
# Identity names use the 'ca-' prefix as defined in ieee1588-ptp-tt@2023-08-14.yang.
# 0xfe (unknown) has no corresponding identity and is omitted by returning None.
_CLOCK_ACCURACY_MAP = {
0x17: "ieee1588-ptp-tt:ca-time-accurate-to-1000-fs",
0x18: "ieee1588-ptp-tt:ca-time-accurate-to-2500-fs",
0x19: "ieee1588-ptp-tt:ca-time-accurate-to-10-ps",
0x1a: "ieee1588-ptp-tt:ca-time-accurate-to-25ps",
0x1b: "ieee1588-ptp-tt:ca-time-accurate-to-100-ps",
0x1c: "ieee1588-ptp-tt:ca-time-accurate-to-250-ps",
0x1d: "ieee1588-ptp-tt:ca-time-accurate-to-1000-ps",
0x1e: "ieee1588-ptp-tt:ca-time-accurate-to-2500-ps",
0x1f: "ieee1588-ptp-tt:ca-time-accurate-to-10-ns",
0x20: "ieee1588-ptp-tt:ca-time-accurate-to-25-ns",
0x21: "ieee1588-ptp-tt:ca-time-accurate-to-100-ns",
0x22: "ieee1588-ptp-tt:ca-time-accurate-to-250-ns",
0x23: "ieee1588-ptp-tt:ca-time-accurate-to-1000-ns",
0x24: "ieee1588-ptp-tt:ca-time-accurate-to-2500-ns",
0x25: "ieee1588-ptp-tt:ca-time-accurate-to-10-us",
0x26: "ieee1588-ptp-tt:ca-time-accurate-to-25-us",
0x27: "ieee1588-ptp-tt:ca-time-accurate-to-100-us",
0x28: "ieee1588-ptp-tt:ca-time-accurate-to-250-us",
0x29: "ieee1588-ptp-tt:ca-time-accurate-to-1000-us",
0x2a: "ieee1588-ptp-tt:ca-time-accurate-to-2500-us",
0x2b: "ieee1588-ptp-tt:ca-time-accurate-to-10-ms",
0x2c: "ieee1588-ptp-tt:ca-time-accurate-to-25-ms",
0x2d: "ieee1588-ptp-tt:ca-time-accurate-to-100-ms",
0x2e: "ieee1588-ptp-tt:ca-time-accurate-to-250-ms",
0x2f: "ieee1588-ptp-tt:ca-time-accurate-to-1-s",
0x30: "ieee1588-ptp-tt:ca-time-accurate-to-10-s",
0x31: "ieee1588-ptp-tt:ca-time-accurate-to-gt-10-s",
}
def _clock_accuracy_identity(raw):
"""Return the YANG identity string for a pmc clockAccuracy hex value, or None."""
try:
return _CLOCK_ACCURACY_MAP.get(int(raw, 16))
except (ValueError, TypeError):
return None
# ---------------------------------------------------------------------------
# time-source identity mapping
# ---------------------------------------------------------------------------
_TIME_SOURCE_MAP = {
"0x10": "ieee1588-ptp-tt:atomic-clock",
"0x20": "ieee1588-ptp-tt:gnss",
"0x30": "ieee1588-ptp-tt:terrestrial-radio",
"0x39": "ieee1588-ptp-tt:serial-time-code",
"0x40": "ieee1588-ptp-tt:ptp",
"0x50": "ieee1588-ptp-tt:ntp",
"0x60": "ieee1588-ptp-tt:hand-set",
"0x90": "ieee1588-ptp-tt:other",
"0xa0": "ieee1588-ptp-tt:internal-oscillator",
}
def _time_source_identity(raw):
return _TIME_SOURCE_MAP.get(raw.lower(),
"ieee1588-ptp-tt:internal-oscillator")
# ---------------------------------------------------------------------------
# delay-mechanism and port-state mapping
# ---------------------------------------------------------------------------
_DELAY_MECH_MAP = {
"E2E": "e2e",
"P2P": "p2p",
"AUTO": "no-mechanism",
}
_PORT_STATE_MAP = {
"INITIALIZING": "initializing",
"FAULTY": "faulty",
"DISABLED": "disabled",
"LISTENING": "listening",
"PRE_MASTER": "pre-time-transmitter",
"MASTER": "time-transmitter",
"PASSIVE": "passive",
"UNCALIBRATED": "uncalibrated",
"SLAVE": "time-receiver",
"GRAND_MASTER": "time-transmitter",
}
# ---------------------------------------------------------------------------
# Per-dataset builders
# ---------------------------------------------------------------------------
def _build_default_ds(d):
"""Map pmc DEFAULT_DATA_SET response to YANG default-ds."""
ds = {}
cid = d.get("clockIdentity")
if cid:
ds["clock-identity"] = _fmt_clock_identity(cid)
v = d.get("numberPorts")
if v:
ds["number-ports"] = int(v)
cq = {}
v = d.get("clockClass")
if v:
cc = _clock_class_identity(v)
if cc:
cq["clock-class"] = cc
v = d.get("clockAccuracy")
if v:
ca = _clock_accuracy_identity(v)
if ca:
cq["clock-accuracy"] = ca
v = d.get("offsetScaledLogVariance")
if v:
cq["offset-scaled-log-variance"] = int(v, 16)
if cq:
ds["clock-quality"] = cq
v = d.get("priority1")
if v:
ds["priority1"] = int(v)
v = d.get("priority2")
if v:
ds["priority2"] = int(v)
v = d.get("domainNumber")
if v:
ds["domain-number"] = int(v)
v = d.get("clientOnly") or d.get("slaveOnly") # renamed in ptp4l 4.x
if v is not None:
ds["time-receiver-only"] = (v == "1")
# instance-type: derive from ptp4l GM/time-receiver state (read-only, operational)
# pmc doesn't directly expose clockType in DEFAULT_DATA_SET
# We'll fill instance-type later from the instance's config if possible
return ds
def _build_current_ds(d):
"""Map pmc CURRENT_DATA_SET response to YANG current-ds."""
ds = {}
v = d.get("stepsRemoved")
if v:
ds["steps-removed"] = int(v)
v = d.get("offsetFromMaster")
if v:
# ptp4l reports nanoseconds as float; YANG time-interval is ns * 2^16.
# RFC 7951: int64 must be JSON-encoded as a string.
try:
ds["offset-from-time-transmitter"] = str(int(float(v) * 65536))
except ValueError:
pass
v = d.get("meanPathDelay")
if v:
try:
ds["mean-delay"] = str(int(float(v) * 65536))
except ValueError:
pass
return ds
def _build_parent_ds(d):
"""Map pmc PARENT_DATA_SET response to YANG parent-ds."""
ds = {}
v = d.get("parentPortIdentity")
if v:
ds["parent-port-identity"] = _fmt_port_identity(v)
v = d.get("parentStats")
if v:
ds["parent-stats"] = (v == "1")
v = d.get("observedParentOffsetScaledLogVariance")
if v:
try:
ds["observed-parent-offset-scaled-log-variance"] = int(v, 16)
except ValueError:
pass
v = d.get("observedParentClockPhaseChangeRate")
if v:
try:
ds["observed-parent-clock-phase-change-rate"] = int(v)
except ValueError:
pass
v = d.get("grandmasterIdentity")
if v:
ds["grandmaster-identity"] = _fmt_clock_identity(v)
gcq = {}
v = d.get("gm.ClockClass")
if v:
cc = _clock_class_identity(v)
if cc:
gcq["clock-class"] = cc
v = d.get("gm.ClockAccuracy")
if v:
ca = _clock_accuracy_identity(v)
if ca:
gcq["clock-accuracy"] = ca
v = d.get("gm.OffsetScaledLogVariance")
if v:
try:
gcq["offset-scaled-log-variance"] = int(v, 16)
except ValueError:
pass
if gcq:
ds["grandmaster-clock-quality"] = gcq
v = d.get("grandmasterPriority1")
if v:
ds["grandmaster-priority1"] = int(v)
v = d.get("grandmasterPriority2")
if v:
ds["grandmaster-priority2"] = int(v)
return ds
def _build_time_properties_ds(d):
"""Map pmc TIME_PROPERTIES_DATA_SET response to YANG time-properties-ds."""
ds = {}
# current-utc-offset has a when condition requiring current-utc-offset-valid='true'
if d.get("currentUtcOffsetValid") in ("1", "true"):
v = d.get("currentUtcOffset")
if v:
ds["current-utc-offset"] = int(v)
v = d.get("leap61")
if v is not None:
ds["leap61"] = (v == "1")
v = d.get("leap59")
if v is not None:
ds["leap59"] = (v == "1")
v = d.get("currentUtcOffsetValid")
if v is not None:
ds["current-utc-offset-valid"] = (v == "1")
v = d.get("ptpTimescale")
if v is not None:
ds["ptp-timescale"] = (v == "1")
v = d.get("timeTraceable")
if v is not None:
ds["time-traceable"] = (v == "1")
v = d.get("frequencyTraceable")
if v is not None:
ds["frequency-traceable"] = (v == "1")
v = d.get("timeSource")
if v:
ds["time-source"] = _time_source_identity(v)
return ds
def _build_port_ds(d):
"""Map pmc PORT_DATA_SET response to YANG port-ds."""
ds = {}
v = d.get("portIdentity")
if v:
ds["port-identity"] = _fmt_port_identity(v)
v = d.get("portState")
if v:
ds["port-state"] = _PORT_STATE_MAP.get(v, "disabled")
v = d.get("logMinDelayReqInterval")
if v:
try:
ds["log-min-delay-req-interval"] = int(v)
except ValueError:
pass
v = d.get("peerMeanPathDelay")
if v:
try:
# RFC 7951: int64 must be JSON-encoded as a string.
ds["mean-link-delay"] = str(int(float(v) * 65536))
except ValueError:
pass
v = d.get("logAnnounceInterval")
if v:
try:
ds["log-announce-interval"] = int(v)
except ValueError:
pass
v = d.get("announceReceiptTimeout")
if v:
try:
ds["announce-receipt-timeout"] = int(v)
except ValueError:
pass
v = d.get("logSyncInterval")
if v:
try:
ds["log-sync-interval"] = int(v)
except ValueError:
pass
v = d.get("delayMechanism")
if v:
ds["delay-mechanism"] = _DELAY_MECH_MAP.get(v, "e2e")
v = d.get("logMinPdelayReqInterval")
if v:
try:
ds["log-min-pdelay-req-interval"] = int(v)
except ValueError:
pass
v = d.get("versionNumber")
if v:
try:
ds["version-number"] = int(v)
except ValueError:
pass
v = d.get("portEnable")
if v is not None:
ds["port-enable"] = (v == "1")
return ds
def _build_port_stats(d):
"""Map pmc PORT_STATS_NP response to ieee802-dot1as-gptp port-statistics-ds."""
stats = {}
mapping = {
"rx_Sync": "rx-sync-count",
"rx_Follow_Up": "rx-follow-up-count",
"rx_Pdelay_Req": "rx-pdelay-req-count",
"rx_Pdelay_Resp": "rx-pdelay-resp-count",
"rx_Pdelay_Resp_Follow_Up": "rx-pdelay-resp-follow-up-count",
"rx_Announce": "rx-announce-count",
"tx_Sync": "tx-sync-count",
"tx_Follow_Up": "tx-follow-up-count",
"tx_Pdelay_Req": "tx-pdelay-req-count",
"tx_Pdelay_Resp": "tx-pdelay-resp-count",
"tx_Pdelay_Resp_Follow_Up": "tx-pdelay-resp-follow-up-count",
"tx_Announce": "tx-announce-count",
}
for pmc_key, yang_key in mapping.items():
v = d.get(pmc_key)
if v is not None:
try:
stats[yang_key] = int(v)
except ValueError:
pass
return stats
# ---------------------------------------------------------------------------
# Per-instance builder
# ---------------------------------------------------------------------------
def _port_interfaces(conf_path):
"""Return ordered list of interface names from ptp4l conf (non-global section headers)."""
ifaces = []
try:
with open(conf_path) as f:
for line in f:
s = line.strip()
if s.startswith('[') and s.endswith(']') and s[1:-1] != 'global':
ifaces.append(s[1:-1])
except OSError:
pass
return ifaces
def _instance_type_from_config(conf_path):
"""Read instance-type from a saved config file (best effort)."""
try:
with open(conf_path, "r") as f:
for line in f:
m = re.match(r'\s*clockType\s+(\S+)', line)
if m:
ct = m.group(1).upper()
if ct == "P2P_TC":
return "p2p-tc"
if ct == "E2E_TC":
return "e2e-tc"
if ct == "BOUNDARY_CLOCK":
return "bc"
# Default: if more than one port, bc; otherwise oc
# (approximation — proper detection requires DEFAULT_DATA_SET numberPorts)
except OSError:
pass
return None
def _build_instance(idx, conf_path):
"""Build one instance dict from pmc queries for instance index idx."""
inst = {"instance-index": idx}
# default-ds
dd = _pmc_get_one(conf_path, "DEFAULT_DATA_SET")
if dd:
dds = _build_default_ds(dd)
# Derive instance-type from numberPorts + config file
num_ports = int(dd.get("numberPorts", "0") or "0")
it = _instance_type_from_config(conf_path)
if it is None:
it = "bc" if num_ports > 1 else "oc"
dds["instance-type"] = it
inst["default-ds"] = dds
# current-ds
cd = _pmc_get_one(conf_path, "CURRENT_DATA_SET")
if cd:
cds = _build_current_ds(cd)
if cds:
inst["current-ds"] = cds
# parent-ds
pd = _pmc_get_one(conf_path, "PARENT_DATA_SET")
if pd:
pds = _build_parent_ds(pd)
if pds:
inst["parent-ds"] = pds
# time-properties-ds
tp = _pmc_get_one(conf_path, "TIME_PROPERTIES_DATA_SET")
if tp:
tpds = _build_time_properties_ds(tp)
if tpds:
inst["time-properties-ds"] = tpds
# ports: PORT_DATA_SET returns one block per port
port_blocks = _pmc_get(conf_path, "PORT_DATA_SET")
stats_blocks = _pmc_get(conf_path, "PORT_STATS_NP")
ifaces = _port_interfaces(conf_path)
# Build a stats map keyed by portIdentity for quick lookup
stats_by_id = {}
for sb in stats_blocks:
pid = sb.get("portIdentity")
if pid:
stats_by_id[pid] = _build_port_stats(sb)
ports = []
for i, pb in enumerate(port_blocks, start=1):
pid_raw = pb.get("portIdentity", "")
port_entry = {}
# port-index = port number from portIdentity
pid_dict = _fmt_port_identity(pid_raw)
port_entry["port-index"] = pid_dict.get("port-number", i)
if i <= len(ifaces):
port_entry["underlying-interface"] = ifaces[i - 1]
pds = _build_port_ds(pb)
if pds:
port_entry["port-ds"] = pds
# 802.1AS port-statistics-ds
stats = stats_by_id.get(pid_raw)
if stats:
port_entry["ieee802-dot1as-gptp:port-statistics-ds"] = stats
ports.append(port_entry)
if ports:
inst["ports"] = {"port": ports}
return inst
# ---------------------------------------------------------------------------
# Top-level entry point
# ---------------------------------------------------------------------------
def operational():
"""Return operational data for ieee1588-ptp-tt."""
out = {}
instances = []
conf_files = sorted(glob.glob("/etc/linuxptp/ptp4l-*.conf"))
for conf_path in conf_files:
m = re.search(r'ptp4l-(\d+)\.conf$', conf_path)
if not m:
continue
idx = int(m.group(1))
# Only include instances with a live UDS socket (i.e. ptp4l running)
uds_path = f"/var/run/ptp4l-{idx}"
if not HOST.exists(uds_path):
continue
try:
inst = _build_instance(idx, conf_path)
instances.append(inst)
except Exception as e:
LOG.debug("ptp4l-%d: skipping instance: %s", idx, e)
if instances:
insert(out, "ieee1588-ptp-tt:ptp", "instances", "instance", instances)
return out
@@ -117,9 +117,38 @@ def interface_common(iplink, ipaddr):
return interface
def interface(iplink, ipaddr):
def ptp_capabilities(ifname, systemjson):
"""Return infix-interfaces:ptp-capabilities dict for ifname, or None."""
caps = systemjson.get("interfaces", {}).get(ifname, {}).get("ptp-capabilities")
if not caps:
return None
result = {}
if cl := caps.get("capabilities"):
result["capabilities"] = cl
if (phc := caps.get("phc-index")) is not None:
result["phc-index"] = phc
if tx := caps.get("tx-types"):
result["tx-types"] = tx
if rx := caps.get("rx-filters"):
result["rx-filters"] = rx
if (idx := caps.get("hwtstamp-provider-index")) is not None:
result["hwtstamp-provider-index"] = idx
if qual := caps.get("hwtstamp-provider-qualifier"):
result["hwtstamp-provider-qualifier"] = qual
return result or None
def interface(iplink, ipaddr, systemjson=None):
interface = interface_common(iplink, ipaddr)
if systemjson is None:
systemjson = {}
if ptpcap := ptp_capabilities(iplink["ifname"], systemjson):
interface["infix-interfaces:ptp-capabilities"] = ptpcap
match interface["type"]:
case "infix-if-type:bridge":
if br := bridge.bridge(iplink):
@@ -163,8 +192,11 @@ def interface(iplink, ipaddr):
def interfaces(ifname=None):
from ..host import HOST
links = common.iplinks(ifname)
addrs = common.ipaddrs(ifname)
systemjson = HOST.read_json("/run/system.json", {})
interfaces = []
for ifname, iplink in links.items():
@@ -177,6 +209,6 @@ def interfaces(ifname=None):
ipaddr = addrs.get(ifname, {})
interfaces.append(interface(iplink, ipaddr))
interfaces.append(interface(iplink, ipaddr, systemjson))
return interfaces
+4 -1
View File
@@ -52,6 +52,7 @@
#define XPATH_LLDP_BASE "/ieee802-dot1ab-lldp:lldp"
#define XPATH_FIREWALL_BASE "/infix-firewall:firewall"
#define XPATH_NTP_BASE "/ietf-ntp:ntp"
#define XPATH_PTP_BASE "/ieee1588-ptp-tt:ptp"
TAILQ_HEAD(sub_head, sub);
@@ -111,7 +112,7 @@ static int ly_add_yanger_data(const struct ly_ctx *ctx, struct lyd_node **parent
err = lyd_parse_data_fd(ctx, fd, LYD_JSON, LYD_PARSE_ONLY, 0, parent);
if (err)
ERROR("Error, parsing yanger data (%d)", err);
ERROR("Error, parsing yanger data (%d): %s", err, ly_errmsg(ctx));
fclose(stream);
/* Note: fclose() already closes the underlying fd from fdopen() */
@@ -455,6 +456,8 @@ static int subscribe_to_all(struct statd *statd)
return SR_ERR_INTERNAL;
if (subscribe(statd, "ietf-ntp", XPATH_NTP_BASE, sr_generic_cb))
return SR_ERR_INTERNAL;
if (subscribe(statd, "ieee1588-ptp-tt", XPATH_PTP_BASE, sr_generic_cb))
return SR_ERR_INTERNAL;
INFO("Successfully subscribed to all models");
return SR_ERR_OK;