@@ -21,6 +21,9 @@ import (
2121 "errors"
2222 "fmt"
2323 "io"
24+ "net"
25+ "strconv"
26+ "strings"
2427)
2528
2629var (
@@ -69,6 +72,80 @@ type flowDirective struct {
6972 flow string
7073}
7174
75+ // PortAttr contains the ofport number and MAC address for an interface.
76+ type PortAttr struct {
77+ // ofPort specifies the OpenFlow port number.
78+ ofPort int
79+
80+ // macAddress specifies the MAC address of the interface.
81+ macAddress string
82+ }
83+
84+ // PortMapping contains the interface name, ofport number and MAC address for an interface.
85+ type PortMapping struct {
86+ portAttr PortAttr
87+ ifName string
88+ }
89+
90+ // UnmarshalText unmarshals a PortMapping from textual form as output by
91+ // 'ovs-ofctl show':
92+ //
93+ // 7(interface1): addr:fe:4f:76:09:88:2b
94+ func (p * PortMapping ) UnmarshalText (b []byte ) error {
95+ // Make a copy per documentation for encoding.TextUnmarshaler.
96+ s := strings .TrimSpace (string (b ))
97+
98+ // Expected format: " 7(interface1): addr:fe:4f:76:09:88:2b"
99+ // Find the opening parenthesis
100+ openParen := strings .IndexByte (s , '(' )
101+ if openParen == - 1 {
102+ return fmt .Errorf ("invalid port mapping format" )
103+ }
104+
105+ // Find the closing parenthesis in the full string
106+ closeParen := strings .IndexByte (s , ')' )
107+ if closeParen == - 1 || closeParen <= openParen {
108+ return fmt .Errorf ("invalid port mapping format" )
109+ }
110+
111+ // Extract interface name (between parentheses)
112+ ifName := s [openParen + 1 : closeParen ]
113+
114+ // Extract ofport number (before opening parenthesis)
115+ ofportStr := strings .TrimSpace (s [:openParen ])
116+ ofport , err := strconv .Atoi (ofportStr )
117+ if err != nil {
118+ return fmt .Errorf ("invalid ofport number: %w" , err )
119+ }
120+
121+ // Validate ofport is in valid OpenFlow port range [0, 65535]
122+ if ofport < 0 || ofport > 65535 {
123+ return fmt .Errorf ("ofport %d out of valid range [0, 65535]" , ofport )
124+ }
125+
126+ // Find "addr:" after the closing parenthesis in the full string
127+ addrPrefix := ": addr:"
128+ addrIdx := strings .Index (s , addrPrefix )
129+ if addrIdx == - 1 || addrIdx <= closeParen {
130+ return fmt .Errorf ("invalid port mapping format" )
131+ }
132+ addrIdx += len (addrPrefix )
133+
134+ // Extract MAC address (after "addr:")
135+ macAddress := strings .TrimSpace (s [addrIdx :])
136+
137+ // Validate MAC address format
138+ if _ , err := net .ParseMAC (macAddress ); err != nil {
139+ return fmt .Errorf ("invalid MAC address %q: %w" , macAddress , err )
140+ }
141+
142+ p .portAttr .ofPort = ofport
143+ p .portAttr .macAddress = macAddress
144+ p .ifName = ifName
145+
146+ return nil
147+ }
148+
72149// Possible flowDirective directive values.
73150const (
74151 dirAdd = "add"
@@ -324,6 +401,80 @@ func (o *OpenFlowService) DumpAggregate(bridge string, flow *MatchFlow) (*FlowSt
324401 return stats , nil
325402}
326403
404+ // DumpPortMapping retrieves port mapping (ofport and MAC address) for a
405+ // specific interface from the specified bridge.
406+ func (o * OpenFlowService ) DumpPortMapping (bridge string , interfaceName string ) (* PortAttr , error ) {
407+ mappings , err := o .DumpPortMappings (bridge )
408+ if err != nil {
409+ return nil , err
410+ }
411+
412+ mapping , ok := mappings [interfaceName ]
413+ if ! ok {
414+ return nil , fmt .Errorf ("interface %q not found" , interfaceName )
415+ }
416+
417+ return mapping , nil
418+ }
419+
420+ // DumpPortMappings retrieves port mappings (ofport and MAC address) for all
421+ // interfaces from the specified bridge. The output is parsed from 'ovs-ofctl show' command.
422+ func (o * OpenFlowService ) DumpPortMappings (bridge string ) (map [string ]* PortAttr , error ) {
423+ args := []string {"show" , bridge }
424+ args = append (args , o .c .ofctlFlags ... )
425+
426+ out , err := o .exec (args ... )
427+ if err != nil {
428+ return nil , err
429+ }
430+
431+ mappings := make (map [string ]* PortAttr )
432+ err = parseEachPort (out , showPrefix , func (line []byte ) error {
433+ // Parse the PortMapping - UnmarshalText validates format and extracts all fields
434+ pm := new (PortMapping )
435+ if err := pm .UnmarshalText (line ); err != nil {
436+ // Skip malformed lines
437+ return nil
438+ }
439+
440+ // Use interface name from PortMapping as map key, copy PortAttr values
441+ mappings [pm .ifName ] = & PortAttr {
442+ ofPort : pm .portAttr .ofPort ,
443+ macAddress : pm .portAttr .macAddress ,
444+ }
445+ return nil
446+ })
447+
448+ if err != nil {
449+ return nil , fmt .Errorf ("failed to parse port mappings: %w" , err )
450+ }
451+
452+ return mappings , nil
453+ }
454+
455+ // DumpPortMappingsByIndex retrieves port mappings keyed by interface index instead of
456+ // interface name. This is useful for populating zone maps in eBPF programs where the
457+ // zone corresponds to the ofport and the map key is the interface index.
458+ // Returns a map of interface index to ofport number.
459+ func (o * OpenFlowService ) DumpPortMappingsByIndex (bridge string ) (map [int ]int , error ) {
460+ mappings , err := o .DumpPortMappings (bridge )
461+ if err != nil {
462+ return nil , err
463+ }
464+
465+ indexMap := make (map [int ]int )
466+ for ifName , portAttr := range mappings {
467+ iface , err := net .InterfaceByName (ifName )
468+ if err != nil {
469+ // Skip interfaces that don't exist or can't be resolved
470+ continue
471+ }
472+ indexMap [iface .Index ] = portAttr .ofPort
473+ }
474+
475+ return indexMap , nil
476+ }
477+
327478var (
328479 // dumpPortsPrefix is a sentinel value returned at the beginning of
329480 // the output from 'ovs-ofctl dump-ports'.
@@ -343,6 +494,10 @@ var (
343494 // dumpAggregatePrefix is a sentinel value returned at the beginning of
344495 // the output from "ovs-ofctl dump-aggregate"
345496 //dumpAggregatePrefix = []byte("NXST_AGGREGATE reply")
497+
498+ // showPrefix is a sentinel value returned at the beginning of
499+ // the output from 'ovs-ofctl show'.
500+ showPrefix = []byte ("OFPT_FEATURES_REPLY" )
346501)
347502
348503// dumpPorts calls 'ovs-ofctl dump-ports' with the specified arguments and
@@ -497,6 +652,39 @@ func parseEach(in []byte, prefix []byte, fn func(b []byte) error) error {
497652 return scanner .Err ()
498653}
499654
655+ // parseEachPort parses ovs-ofctl show output from the input buffer, ensuring it has the
656+ // specified prefix, and invoking the input function on each line scanned.
657+ func parseEachPort (in []byte , prefix []byte , fn func (b []byte ) error ) error {
658+ // Handle empty input - return nil to allow empty mappings (matches test expectations)
659+ if len (in ) == 0 {
660+ return nil
661+ }
662+
663+ // First line must not be empty
664+ scanner := bufio .NewScanner (bytes .NewReader (in ))
665+ scanner .Split (bufio .ScanLines )
666+ if ! scanner .Scan () {
667+ return io .ErrUnexpectedEOF
668+ }
669+
670+ // First line must contain the prefix returned by OVS
671+ if ! bytes .Contains (scanner .Bytes (), prefix ) {
672+ return io .ErrUnexpectedEOF
673+ }
674+
675+ // Scan every line to retrieve information needed to unmarshal
676+ // a single PortMapping struct.
677+ for scanner .Scan () {
678+ b := make ([]byte , len (scanner .Bytes ()))
679+ copy (b , scanner .Bytes ())
680+ if err := fn (b ); err != nil {
681+ return err
682+ }
683+ }
684+
685+ return scanner .Err ()
686+ }
687+
500688// exec executes an ExecFunc using 'ovs-ofctl'.
501689func (o * OpenFlowService ) exec (args ... string ) ([]byte , error ) {
502690 return o .c .exec ("ovs-ofctl" , args ... )
0 commit comments