Skip to content

Commit 9668b0a

Browse files
committed
Implement go-openvswitch API for ofport/interface mapping
1 parent 045aa1d commit 9668b0a

File tree

2 files changed

+600
-0
lines changed

2 files changed

+600
-0
lines changed

ovs/openflow.go

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import (
2121
"errors"
2222
"fmt"
2323
"io"
24+
"net"
25+
"strconv"
26+
"strings"
2427
)
2528

2629
var (
@@ -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.
73150
const (
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+
327478
var (
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'.
501689
func (o *OpenFlowService) exec(args ...string) ([]byte, error) {
502690
return o.c.exec("ovs-ofctl", args...)

0 commit comments

Comments
 (0)