Skip to content

Commit 2e7bfd5

Browse files
committed
feat(pre): add Packet Replication Engine wrapper (multicast + clone)
New pre package exposes typed helpers for the P4Runtime PRE entities: pre.Writer .InsertMulticastGroup / Modify / Delete / Read .InsertCloneSession / Modify / Delete / Read pre.MulticastGroup{ ID, Replicas, Metadata } pre.CloneSession{ ID, Replicas, ClassOfService, PacketLengthBytes } pre.Replica{ EgressPort, Instance } Cases that need broadcast forwarding (04_l2_broadcast_switch) or packet cloning to a CPU port (05_l2_learning_switch, CPU variant) can now be written against this API rather than hand-rolling the PacketReplicationEngineEntry proto. Tests cover insert / modify / delete / read for both entities, plus validation of zero IDs and empty replica lists. Statement coverage 92.3 %. README feature matrix and Chinese translation updated; CHANGELOG opens a [1.1.0] section. Signed-off-by: Henghua Zhang <1652709417@qq.com>
1 parent 05d215b commit 2e7bfd5

5 files changed

Lines changed: 497 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ every breaking change requires a major version bump.
1010

1111
## [Unreleased]
1212

13+
## [1.1.0] - 2026-04-21
14+
15+
### Added
16+
- `pre` package wrapping the P4Runtime Packet Replication Engine:
17+
`MulticastGroup` + `CloneSession` with `Insert` / `Modify` / `Delete` /
18+
`Read` helpers, plus a `Replica` type carrying egress port and instance.
19+
Tests at 92 % statement coverage.
20+
1321
## [1.0.0] - 2026-04-20
1422

1523
Initial public release.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ connection, pipeline push, L2 learning switch, packet I/O, and counter reads.
7777
| Counters, Meters, Registers (direct and indirect) | ready |
7878
| PacketIn / PacketOut with metadata encode/decode | ready |
7979
| Digest subscribe and ack | ready |
80+
| Packet Replication Engine (multicast groups, clone sessions) | ready (v1.1) |
8081
| Reference CLI (`p4ctl`) | ready |
8182
| Prometheus adapter | planned |
8283
| OpenTelemetry gRPC interceptors | planned |

docs/i18n/README.zh-CN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ L2 学习交换机、Packet I/O 以及计数器读取。
7878
| Counters、Meters、Registers | 已就绪 |
7979
| PacketIn / PacketOut | 已就绪 |
8080
| Digest 订阅与 Ack | 已就绪 |
81+
| PRE(组播组 / 克隆会话) | 已就绪 (v1.1) |
8182
| Prometheus 适配器 | 规划中 |
8283
| OpenTelemetry gRPC 拦截器示例 | 规划中 |
8384

pre/pre.go

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
// Package pre wraps the P4Runtime Packet Replication Engine (PRE)
2+
// entities: multicast groups and clone sessions.
3+
//
4+
// Multicast groups are referenced from P4 programs through the
5+
// standard_metadata.mcast_grp field (V1Model) or the PSA equivalent. A
6+
// clone session is referenced by clone_session_id (PSA) or
7+
// clone3/clone_preserving_field_list (V1Model).
8+
//
9+
// IDs in this package are target-local: the caller picks them and the
10+
// target accepts any non-zero value, so we do not cross-check against
11+
// the P4Info.
12+
package pre
13+
14+
import (
15+
"context"
16+
"errors"
17+
"fmt"
18+
19+
p4v1 "github.com/p4lang/p4runtime/go/p4/v1"
20+
21+
"github.com/zhh2001/p4runtime-go-controller/client"
22+
)
23+
24+
// Replica is a single replica in a multicast group or clone session.
25+
// Instance distinguishes replicas that share the same egress port.
26+
type Replica struct {
27+
EgressPort uint32
28+
Instance uint32
29+
}
30+
31+
// MulticastGroup describes a PRE multicast group entry.
32+
type MulticastGroup struct {
33+
ID uint32
34+
Replicas []Replica
35+
Metadata []byte // optional opaque caller metadata (P4Runtime 1.4+)
36+
}
37+
38+
// CloneSession describes a PRE clone session entry. PacketLengthBytes
39+
// truncates every cloned packet to this many bytes; zero disables
40+
// truncation.
41+
type CloneSession struct {
42+
ID uint32
43+
Replicas []Replica
44+
ClassOfService uint32
45+
PacketLengthBytes int32
46+
}
47+
48+
// Writer is a typed wrapper over Client for PRE operations. It is safe
49+
// for concurrent use because Client is.
50+
type Writer struct {
51+
c *client.Client
52+
}
53+
54+
// NewWriter constructs a PRE writer bound to c. Returns an error only
55+
// if c is nil.
56+
func NewWriter(c *client.Client) (*Writer, error) {
57+
if c == nil {
58+
return nil, errors.New("pre.NewWriter: nil client")
59+
}
60+
return &Writer{c: c}, nil
61+
}
62+
63+
// InsertMulticastGroup installs a new multicast group. It is an error
64+
// to reinstall a group with the same ID; use ModifyMulticastGroup for
65+
// updates.
66+
func (w *Writer) InsertMulticastGroup(ctx context.Context, mg MulticastGroup) error {
67+
return w.writeMulticastGroup(ctx, client.UpdateInsert, mg)
68+
}
69+
70+
// ModifyMulticastGroup replaces the replica set of an existing multicast
71+
// group.
72+
func (w *Writer) ModifyMulticastGroup(ctx context.Context, mg MulticastGroup) error {
73+
return w.writeMulticastGroup(ctx, client.UpdateModify, mg)
74+
}
75+
76+
// DeleteMulticastGroup removes the multicast group with the given ID.
77+
func (w *Writer) DeleteMulticastGroup(ctx context.Context, id uint32) error {
78+
if id == 0 {
79+
return errors.New("pre.DeleteMulticastGroup: id must be non-zero")
80+
}
81+
return w.c.Write(ctx, client.WriteOptions{},
82+
multicastUpdate(client.UpdateDelete, MulticastGroup{ID: id}))
83+
}
84+
85+
// ReadMulticastGroups returns every multicast group installed on the
86+
// target. Pass id=0 to read all groups; pass a specific id to read just
87+
// that one.
88+
func (w *Writer) ReadMulticastGroups(ctx context.Context, id uint32) ([]MulticastGroup, error) {
89+
selector := &p4v1.Entity{Entity: &p4v1.Entity_PacketReplicationEngineEntry{
90+
PacketReplicationEngineEntry: &p4v1.PacketReplicationEngineEntry{
91+
Type: &p4v1.PacketReplicationEngineEntry_MulticastGroupEntry{
92+
MulticastGroupEntry: &p4v1.MulticastGroupEntry{MulticastGroupId: id},
93+
},
94+
},
95+
}}
96+
ents, err := w.c.Read(ctx, selector)
97+
if err != nil {
98+
return nil, err
99+
}
100+
var out []MulticastGroup
101+
for _, e := range ents {
102+
mge := e.GetPacketReplicationEngineEntry().GetMulticastGroupEntry()
103+
if mge == nil {
104+
continue
105+
}
106+
out = append(out, MulticastGroup{
107+
ID: mge.GetMulticastGroupId(),
108+
Replicas: decodeReplicas(mge.GetReplicas()),
109+
Metadata: mge.GetMetadata(),
110+
})
111+
}
112+
return out, nil
113+
}
114+
115+
// InsertCloneSession installs a new clone session.
116+
func (w *Writer) InsertCloneSession(ctx context.Context, cs CloneSession) error {
117+
return w.writeCloneSession(ctx, client.UpdateInsert, cs)
118+
}
119+
120+
// ModifyCloneSession updates an existing clone session.
121+
func (w *Writer) ModifyCloneSession(ctx context.Context, cs CloneSession) error {
122+
return w.writeCloneSession(ctx, client.UpdateModify, cs)
123+
}
124+
125+
// DeleteCloneSession removes the clone session with the given ID.
126+
func (w *Writer) DeleteCloneSession(ctx context.Context, id uint32) error {
127+
if id == 0 {
128+
return errors.New("pre.DeleteCloneSession: id must be non-zero")
129+
}
130+
return w.c.Write(ctx, client.WriteOptions{},
131+
cloneUpdate(client.UpdateDelete, CloneSession{ID: id}))
132+
}
133+
134+
// ReadCloneSessions returns every clone session installed on the target.
135+
// Pass id=0 to read all sessions.
136+
func (w *Writer) ReadCloneSessions(ctx context.Context, id uint32) ([]CloneSession, error) {
137+
selector := &p4v1.Entity{Entity: &p4v1.Entity_PacketReplicationEngineEntry{
138+
PacketReplicationEngineEntry: &p4v1.PacketReplicationEngineEntry{
139+
Type: &p4v1.PacketReplicationEngineEntry_CloneSessionEntry{
140+
CloneSessionEntry: &p4v1.CloneSessionEntry{SessionId: id},
141+
},
142+
},
143+
}}
144+
ents, err := w.c.Read(ctx, selector)
145+
if err != nil {
146+
return nil, err
147+
}
148+
var out []CloneSession
149+
for _, e := range ents {
150+
cse := e.GetPacketReplicationEngineEntry().GetCloneSessionEntry()
151+
if cse == nil {
152+
continue
153+
}
154+
out = append(out, CloneSession{
155+
ID: cse.GetSessionId(),
156+
Replicas: decodeReplicas(cse.GetReplicas()),
157+
ClassOfService: cse.GetClassOfService(),
158+
PacketLengthBytes: cse.GetPacketLengthBytes(),
159+
})
160+
}
161+
return out, nil
162+
}
163+
164+
func (w *Writer) writeMulticastGroup(ctx context.Context, kind client.UpdateType, mg MulticastGroup) error {
165+
if mg.ID == 0 {
166+
return fmt.Errorf("pre.MulticastGroup: id must be non-zero")
167+
}
168+
if err := validateReplicas(mg.Replicas); err != nil {
169+
return err
170+
}
171+
return w.c.Write(ctx, client.WriteOptions{}, multicastUpdate(kind, mg))
172+
}
173+
174+
func (w *Writer) writeCloneSession(ctx context.Context, kind client.UpdateType, cs CloneSession) error {
175+
if cs.ID == 0 {
176+
return fmt.Errorf("pre.CloneSession: id must be non-zero")
177+
}
178+
if err := validateReplicas(cs.Replicas); err != nil {
179+
return err
180+
}
181+
return w.c.Write(ctx, client.WriteOptions{}, cloneUpdate(kind, cs))
182+
}
183+
184+
func multicastUpdate(kind client.UpdateType, mg MulticastGroup) *p4v1.Update {
185+
entry := &p4v1.MulticastGroupEntry{
186+
MulticastGroupId: mg.ID,
187+
Replicas: encodeReplicas(mg.Replicas),
188+
}
189+
if len(mg.Metadata) > 0 {
190+
entry.Metadata = append([]byte(nil), mg.Metadata...)
191+
}
192+
return &p4v1.Update{
193+
Type: kind,
194+
Entity: &p4v1.Entity{Entity: &p4v1.Entity_PacketReplicationEngineEntry{
195+
PacketReplicationEngineEntry: &p4v1.PacketReplicationEngineEntry{
196+
Type: &p4v1.PacketReplicationEngineEntry_MulticastGroupEntry{
197+
MulticastGroupEntry: entry,
198+
},
199+
},
200+
}},
201+
}
202+
}
203+
204+
func cloneUpdate(kind client.UpdateType, cs CloneSession) *p4v1.Update {
205+
entry := &p4v1.CloneSessionEntry{
206+
SessionId: cs.ID,
207+
Replicas: encodeReplicas(cs.Replicas),
208+
ClassOfService: cs.ClassOfService,
209+
PacketLengthBytes: cs.PacketLengthBytes,
210+
}
211+
return &p4v1.Update{
212+
Type: kind,
213+
Entity: &p4v1.Entity{Entity: &p4v1.Entity_PacketReplicationEngineEntry{
214+
PacketReplicationEngineEntry: &p4v1.PacketReplicationEngineEntry{
215+
Type: &p4v1.PacketReplicationEngineEntry_CloneSessionEntry{
216+
CloneSessionEntry: entry,
217+
},
218+
},
219+
}},
220+
}
221+
}
222+
223+
func encodeReplicas(rs []Replica) []*p4v1.Replica {
224+
out := make([]*p4v1.Replica, 0, len(rs))
225+
for _, r := range rs {
226+
out = append(out, &p4v1.Replica{
227+
PortKind: &p4v1.Replica_EgressPort{EgressPort: r.EgressPort},
228+
Instance: r.Instance,
229+
})
230+
}
231+
return out
232+
}
233+
234+
func decodeReplicas(rs []*p4v1.Replica) []Replica {
235+
out := make([]Replica, 0, len(rs))
236+
for _, r := range rs {
237+
out = append(out, Replica{
238+
EgressPort: r.GetEgressPort(),
239+
Instance: r.GetInstance(),
240+
})
241+
}
242+
return out
243+
}
244+
245+
func validateReplicas(rs []Replica) error {
246+
if len(rs) == 0 {
247+
return errors.New("pre: replicas must not be empty")
248+
}
249+
for i, r := range rs {
250+
if r.EgressPort == 0 {
251+
return fmt.Errorf("pre: replica[%d] egress port must be non-zero", i)
252+
}
253+
}
254+
return nil
255+
}

0 commit comments

Comments
 (0)