diff --git a/ovs/action.go b/ovs/action.go index 3e26067..bee7b87 100644 --- a/ovs/action.go +++ b/ovs/action.go @@ -20,6 +20,7 @@ import ( "fmt" "net" "strconv" + "strings" ) var ( @@ -70,18 +71,28 @@ var ( // errLearnedNil is returned when Learn is called with a nil *LearnedFlow. errLearnedNil = errors.New("learned flow for action learn is nil") + + // errPopFieldEmpty is returned when Pop is called with field set to the empty string + errPopFieldEmpty = errors.New("field for action pop (pop:field syntax) is empty") + + // errPushFieldEmpty is returned when Push is called with field set to the empty string + errPushFieldEmpty = errors.New("field for action push (push:field syntax) is empty") ) // Action strings in lower case, as those are compared to the lower case letters // in parseAction(). const ( - actionAll = "all" - actionDrop = "drop" - actionFlood = "flood" - actionInPort = "in_port" - actionLocal = "local" - actionNormal = "normal" - actionStripVLAN = "strip_vlan" + actionAll = "all" + actionDrop = "drop" + actionFlood = "flood" + actionInPort = "in_port" + actionLocal = "local" + actionNormal = "normal" + actionStripVLAN = "strip_vlan" + actionDecTTL = "dec_ttl" + actionDecTTLNoParam = "dec_ttl()" + actionCTClear = "ct_clear" + actionController = "controller" ) // An Action is a type which can be marshaled into an OpenFlow action. Actions can be @@ -120,6 +131,12 @@ func (a *textAction) GoString() string { return "ovs.Normal()" case actionStripVLAN: return "ovs.StripVLAN()" + case actionDecTTL, actionDecTTLNoParam: + return "ovs.DecTTL()" + case actionCTClear: + return "ovs.CTClear()" + case actionController: + return "ovs.Controller()" default: return fmt.Sprintf("// BUG(mdlayher): unimplemented OVS text action: %q", a.action) } @@ -178,6 +195,13 @@ func StripVLAN() Action { } } +// CTClear clears connection tracking state from the flow +func CTClear() Action { + return &textAction{ + action: actionCTClear, + } +} + // printf-style patterns for marshaling and unmarshaling actions. const ( patConnectionTracking = "ct(%s)" @@ -195,6 +219,12 @@ const ( patResubmitPort = "resubmit:%s" patResubmitPortTable = "resubmit(%s,%s)" patLearn = "learn(%s)" + patPush = "push:%s" + patPop = "pop:%s" + patGroup = "group:%d" + patBundle = "bundle(%s,%d,%s,ofport,members:%s)" + patDecTTL = "dec_ttl" + patDecTTLIds = "dec_ttl(%s)" ) // ConnectionTracking sends a packet through the host's connection tracker. @@ -446,7 +476,7 @@ func (a *outputFieldAction) GoString() string { // applies multipath link selection `algorithm` (with parameter `arg`) // to choose one of `n_links` output links numbered 0 through n_links // minus 1, and stores the link into `dst`, which must be a field or -// subfield in the syntax described under ``Field Specifications’’ +// subfield in the syntax described under “Field Specifications’’ // above. // https://www.openvswitch.org/support/dist-docs/ovs-actions.7.txt func Multipath(fields string, basis int, algorithm string, nlinks int, arg int, dst string) Action { @@ -719,6 +749,253 @@ func (a *learnAction) MarshalText() ([]byte, error) { return bprintf(patLearn, l), nil } +// Push action pushes src on a general-purpose stack +// If either string is empty, an error is returned. +func Push(field string) Action { + return &pushAction{ + field: field, + } +} + +type pushAction struct { + field string +} + +// GoString implements Action. +func (a *pushAction) GoString() string { + return fmt.Sprintf("ovs.Push(%#v)", a.field) +} + +// MarshalText implements Action. +func (a *pushAction) MarshalText() ([]byte, error) { + if a.field == "" { + return nil, errPushFieldEmpty + } + + return bprintf(patPush, a.field), nil +} + +// Pop action pops an entry off the stack into dst +// If either string is empty, an error is returned. +func Pop(field string) Action { + return &popAction{ + field: field, + } +} + +type popAction struct { + field string +} + +// GoString implements Action. +func (a *popAction) GoString() string { + return fmt.Sprintf("ovs.Pop(%#v)", a.field) +} + +// MarshalText implements Action. +func (a *popAction) MarshalText() ([]byte, error) { + if a.field == "" { + return nil, errPopFieldEmpty + } + + return bprintf(patPop, a.field), nil +} + +// Controller sends the packet and its metadata to an OpenFlow controller or controllers +// encapsulated in an OpenFlow packet-in message overwrites the specified field with the specified value. +func Controller(maxLen int, reason string, id int, userdata string, pause bool) Action { + if maxLen == 0 { + maxLen = 65535 + } + return &controllerAction{ + maxLen: maxLen, + reason: reason, + id: id, + userdata: userdata, + pause: pause, + } +} + +type controllerAction struct { + maxLen int + reason string + id int + userdata string + pause bool +} + +// GoString implements Action. +func (a *controllerAction) GoString() string { + + var buf strings.Builder + buf.WriteString("ovs.Controller(") + first := true + if a.maxLen != 65535 { + buf.WriteString(fmt.Sprintf("max_len=%d", a.maxLen)) + first = false + } + if a.reason != "" { + if !first { + buf.WriteString(", ") + } + buf.WriteString(fmt.Sprintf("reason=%s", a.reason)) + first = false + } + if a.id != 0 { + if !first { + buf.WriteString(", ") + } + buf.WriteString(fmt.Sprintf("id=%d", a.id)) + first = false + } + if a.userdata != "" { + if !first { + buf.WriteString(", ") + } + buf.WriteString(fmt.Sprintf("userdata=%s", a.userdata)) + first = false + } + if a.pause { + if !first { + buf.WriteString(", ") + } + buf.WriteString("pause") + first = false + } + buf.WriteString(")") + return buf.String() +} + +func (a *controllerAction) IsZero() bool { + return a.maxLen == 65535 && a.reason == "" && a.id == 0 && a.userdata == "" && !a.pause +} + +func (a *controllerAction) OnlyMaxLen() bool { + return a.maxLen != 65535 && a.reason == "" && a.id == 0 && a.userdata == "" && !a.pause +} + +// MarshalText implements Action. +func (a *controllerAction) MarshalText() ([]byte, error) { + if a.IsZero() { + return bprintf("controller"), nil + } + if a.OnlyMaxLen() { + return bprintf("controller:%d", a.maxLen), nil + } + var buf strings.Builder + buf.WriteString("controller(") + first := true + if a.maxLen != 65535 { + buf.WriteString(fmt.Sprintf("max_len=%d", a.maxLen)) + first = false + } + if a.reason != "" { + if !first { + buf.WriteString(",") + } + buf.WriteString(fmt.Sprintf("reason=%s", a.reason)) + first = false + } + if a.id != 0 { + if !first { + buf.WriteString(",") + } + buf.WriteString(fmt.Sprintf("id=%d", a.id)) + first = false + } + if a.userdata != "" { + if !first { + buf.WriteString(",") + } + buf.WriteString(fmt.Sprintf("userdata=%s", a.userdata)) + first = false + } + if a.pause { + if !first { + buf.WriteString(",") + } + buf.WriteString("pause") + first = false + } + buf.WriteString(")") + return []byte(buf.String()), nil +} + +// Group outputs the packet to the OpenFlow group +func Group(group int) Action { + return &groupAction{ + group: group, + } +} + +type groupAction struct { + group int +} + +// GoString implements Action. +func (a *groupAction) GoString() string { + return fmt.Sprintf("ovs.Group(%d)", a.group) +} + +// MarshalText implements Action. +func (a *groupAction) MarshalText() ([]byte, error) { + return bprintf(patGroup, a.group), nil +} + +// Bundle action choose a port (a member) from a comma-separated OpenFlow +// port list. After selecting the port, bundle outputs to it +func Bundle(fields string, basis int, algorithm string, members ...int) Action { + return &bundleAction{ + fields: fields, + basis: basis, + algorithm: algorithm, + members: members, + } +} + +type bundleAction struct { + fields string + basis int + algorithm string + members []int +} + +// GoString implements Action. +func (a *bundleAction) GoString() string { + return fmt.Sprintf("ovs.Bundle(%s,%d,%s,ofport,members:%s)", a.fields, a.basis, a.algorithm, + formatIntArr(a.members, ", ")) +} + +// MarshalText implements Action. +func (a *bundleAction) MarshalText() ([]byte, error) { + return bprintf(patBundle, a.fields, a.basis, a.algorithm, + formatIntArr(a.members, ",")), nil +} + +// DecTTL decrement TTL of IPv4 packet or hop limit of IPv6 packet +func DecTTL(ids ...int) Action { + return &decTTLAction{ + ids: ids, + } +} + +type decTTLAction struct { + ids []int +} + +// GoString implements Action. +func (a *decTTLAction) GoString() string { + return fmt.Sprintf("ovs.DecTTL(%s)", formatIntArr(a.ids, ", ")) +} + +// MarshalText implements Action. +func (a *decTTLAction) MarshalText() ([]byte, error) { + if len(a.ids) == 0 { + return bprintf(patDecTTL), nil + } + return bprintf(patDecTTLIds, formatIntArr(a.ids, ",")), nil +} + // validARPOP indicates if an ARP OP is out of range. It should be in the range // 1-4. func validARPOP(op uint16) bool { @@ -742,3 +1019,15 @@ func validVLANVID(vid int) bool { func validVLANPCP(pcp int) bool { return pcp >= 0 && pcp <= 7 } + +// formatIntArr return a comma separate string for an int array +func formatIntArr(arr []int, sep string) string { + var buf strings.Builder + for idx, i := range arr { + if idx != 0 { + buf.WriteString(sep) + } + buf.WriteString(strconv.Itoa(i)) + } + return buf.String() +} diff --git a/ovs/action_test.go b/ovs/action_test.go index d58daa5..0c6b0aa 100644 --- a/ovs/action_test.go +++ b/ovs/action_test.go @@ -767,6 +767,264 @@ func TestLearn(t *testing.T) { } } +func TestPopField(t *testing.T) { + var tests = []struct { + desc string + a Action + action string + err error + }{ + { + desc: "pop OK", + a: Pop("NXM_OF_IN_PORT[]"), + action: "pop:NXM_OF_IN_PORT[]", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + action, err := tt.a.MarshalText() + + if want, got := tt.err, err; want != got { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", + want, got) + } + if err != nil { + return + } + + if want, got := tt.action, string(action); want != got { + t.Fatalf("unexpected Action:\n- want: %q\n- got: %q", + want, got) + } + }) + } +} + +func TestPushField(t *testing.T) { + var tests = []struct { + desc string + a Action + action string + err error + }{ + { + desc: "push OK", + a: Push("NXM_NX_REG0[]"), + action: "push:NXM_NX_REG0[]", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + action, err := tt.a.MarshalText() + + if want, got := tt.err, err; want != got { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", + want, got) + } + if err != nil { + return + } + + if want, got := tt.action, string(action); want != got { + t.Fatalf("unexpected Action:\n- want: %q\n- got: %q", + want, got) + } + }) + } +} + +func TestDecTTLField(t *testing.T) { + var tests = []struct { + desc string + a Action + action string + err error + }{ + { + desc: "dec_ttl OK", + a: DecTTL(), + action: "dec_ttl", + }, + { + desc: "dec_ttl 1 id", + a: DecTTL(1), + action: "dec_ttl(1)", + }, + { + desc: "dec_ttl 2 ids", + a: DecTTL(1, 2), + action: "dec_ttl(1,2)", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + action, err := tt.a.MarshalText() + + if want, got := tt.err, err; want != got { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", + want, got) + } + if err != nil { + return + } + + if want, got := tt.action, string(action); want != got { + t.Fatalf("unexpected Action:\n- want: %q\n- got: %q", + want, got) + } + }) + } +} + +func TestControllerField(t *testing.T) { + var tests = []struct { + desc string + a Action + action string + err error + }{ + { + desc: "controller plan", + a: Controller(0, "", 0, "", false), + action: "controller", + }, + { + desc: "controller userdata", + a: Controller(0, "", 0, "00.00.00.04.00.00.00.00", false), + action: "controller(userdata=00.00.00.04.00.00.00.00)", + }, + { + desc: "controller max_len", + a: Controller(10, "", 0, "", false), + action: "controller:10", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + action, err := tt.a.MarshalText() + + if want, got := tt.err, err; want != got { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", + want, got) + } + if err != nil { + return + } + + if want, got := tt.action, string(action); want != got { + t.Fatalf("unexpected Action:\n- want: %q\n- got: %q", + want, got) + } + }) + } +} + +func TestCTClearField(t *testing.T) { + var tests = []struct { + desc string + a Action + action string + err error + }{ + { + desc: "ct_clear ok", + a: CTClear(), + action: "ct_clear", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + action, err := tt.a.MarshalText() + + if want, got := tt.err, err; want != got { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", + want, got) + } + if err != nil { + return + } + + if want, got := tt.action, string(action); want != got { + t.Fatalf("unexpected Action:\n- want: %q\n- got: %q", + want, got) + } + }) + } +} + +func TestGroupField(t *testing.T) { + var tests = []struct { + desc string + a Action + action string + err error + }{ + { + desc: "group ok", + a: Group(1), + action: "group:1", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + action, err := tt.a.MarshalText() + + if want, got := tt.err, err; want != got { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", + want, got) + } + if err != nil { + return + } + + if want, got := tt.action, string(action); want != got { + t.Fatalf("unexpected Action:\n- want: %q\n- got: %q", + want, got) + } + }) + } +} + +func TestBundleField(t *testing.T) { + var tests = []struct { + desc string + a Action + action string + err error + }{ + { + desc: "bundle ok", + a: Bundle("eth_src", 0, "active_backup", 149), + action: "bundle(eth_src,0,active_backup,ofport,members:149)", + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + action, err := tt.a.MarshalText() + + if want, got := tt.err, err; want != got { + t.Fatalf("unexpected error:\n- want: %v\n- got: %v", + want, got) + } + if err != nil { + return + } + + if want, got := tt.action, string(action); want != got { + t.Fatalf("unexpected Action:\n- want: %q\n- got: %q", + want, got) + } + }) + } +} + func TestActionGoString(t *testing.T) { tests := []struct { a Action @@ -859,6 +1117,42 @@ func TestActionGoString(t *testing.T) { }), s: `ovs.Learn(&ovs.LearnedFlow{Priority:0, InPort:0, Matches:[]ovs.Match{ovs.DataLinkType(0x0800)}, Table:0, IdleTimeout:0, Cookie:0x0, Actions:[]ovs.Action{ovs.OutputField("in_port")}, DeleteLearned:true, FinHardTimeout:10, HardTimeout:30, Limit:10})`, }, + { + a: Push("NXM_NX_REG0[]"), + s: `ovs.Push("NXM_NX_REG0[]")`, + }, + { + a: Pop("NXM_NX_REG0[]"), + s: `ovs.Pop("NXM_NX_REG0[]")`, + }, + { + a: DecTTL(1, 2), + s: `ovs.DecTTL(1, 2)`, + }, + { + a: DecTTL(), + s: `ovs.DecTTL()`, + }, + { + a: CTClear(), + s: `ovs.CTClear()`, + }, + { + a: Group(5), + s: `ovs.Group(5)`, + }, + { + a: Controller(0, "", 0, "00.00.00.0c.00.00.00.00.00.19.00.10.80.00.08.06.0e.27.b1.82.65.0c.00.00.00.19.00.18.80.00.34.10.fe.80.00.00.00.00.00.00.0c.27.b1.ff.fe.82.65.0c.00.19.00.18.80.00.3e.10.fe.80.00.00.00.00.00.00.0c.27.b1.ff.fe.82.65.0c.00.19.00.10.80.00.42.06.0e.27.b1.82.65.0c.00.00.00.1c.00.18.00.20.00.00.00.00.00.00.00.01.1c.04.00.01.1e.04.00.00.00.00.00.19.00.10.00.01.15.08.00.00.00.01.00.00.00.01.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.25.00.00.00", false), + s: `ovs.Controller(userdata=00.00.00.0c.00.00.00.00.00.19.00.10.80.00.08.06.0e.27.b1.82.65.0c.00.00.00.19.00.18.80.00.34.10.fe.80.00.00.00.00.00.00.0c.27.b1.ff.fe.82.65.0c.00.19.00.18.80.00.3e.10.fe.80.00.00.00.00.00.00.0c.27.b1.ff.fe.82.65.0c.00.19.00.10.80.00.42.06.0e.27.b1.82.65.0c.00.00.00.1c.00.18.00.20.00.00.00.00.00.00.00.01.1c.04.00.01.1e.04.00.00.00.00.00.19.00.10.00.01.15.08.00.00.00.01.00.00.00.01.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.25.00.00.00)`, + }, + { + a: Controller(10, "", 0, "", false), + s: `ovs.Controller(max_len=10)`, + }, + { + a: Bundle("eth_src", 0, "active_backup", 149), + s: `ovs.Bundle(eth_src,0,active_backup,ofport,members:149)`, + }, } for _, tt := range tests { @@ -869,3 +1163,43 @@ func TestActionGoString(t *testing.T) { }) } } + +func Test_formatIntArr(t *testing.T) { + type arg struct { + arr []int + sep string + } + tests := []struct { + name string + arg arg + want string + }{ + { + "empty arr", + arg{arr: []int{}, sep: ","}, + "", + }, + { + "1 item", + arg{arr: []int{1}, sep: ","}, + "1", + }, + { + "2 item", + arg{arr: []int{1, 2}, sep: ","}, + "1,2", + }, + { + "2 item with space", + arg{arr: []int{1, 2}, sep: ", "}, + "1, 2", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := formatIntArr(tt.arg.arr, tt.arg.sep); got != tt.want { + t.Errorf("formatIntArr() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/ovs/actionparser.go b/ovs/actionparser.go index ef2bbe8..621d963 100644 --- a/ovs/actionparser.go +++ b/ovs/actionparser.go @@ -146,27 +146,73 @@ func (s *stack) pop() { var ( // resubmitRe is the regex used to match the resubmit action // with port and table specified + //Syntax: + // resubmit([port],[table][,ct]) resubmitRe = regexp.MustCompile(`resubmit\((\d*),(\d*)\)`) // resubmitPortRe is the regex used to match the resubmit action // when only a port is specified + //Syntax: + // resubmit:port resubmitPortRe = regexp.MustCompile(`resubmit:(\d+)`) // ctRe is the regex used to match the ct action with its // parameter list. + //Syntax: + // ct([argument]...) + // ct(commit[,argument]...) ctRe = regexp.MustCompile(`ct\((\S+)\)`) // loadRe is the regex used to match the load action // with its parameters. + //Syntax: + // load:value->dst loadRe = regexp.MustCompile(`load:(\S+)->(\S+)`) // moveRe is the regex used to match the move action // with its parameters. + //Syntax: + // move:src->dst moveRe = regexp.MustCompile(`move:(\S+)->(\S+)`) // setFieldRe is the regex used to match the set_field action // with its parameters. + //Syntax: + // set_field:value[/mask]->dst setFieldRe = regexp.MustCompile(`set_field:(\S+)->(\S+)`) + + // popRe is the regex used to match the pop action + // with its parameters. + //Syntax: + // pop:dst + popRe = regexp.MustCompile(`pop:(\S+)`) + + // pushRe is the regex used to match the push action + // with its parameters. + //Syntax: + // push:src + pushRe = regexp.MustCompile(`push:(\S+)`) + + // controllerRe is the regex used to match the controller action + // with its parameters. + //Syntax: + // controller + // controller:max_len + // controller(key[=value], ...) + controllerMaxLenRe = regexp.MustCompile(`controller:(\S+)`) + controllerParamRe = regexp.MustCompile(`controller\((\S+)\)`) + + // groupRe is the regex used to match the controller action + // with its parameters. + //Syntax: + // group:group + groupRe = regexp.MustCompile(`group:(\S+)`) + + // bundleRe is the regex used to match the controller action + // with its parameters. + //Syntax: + // bundle(fields,basis,algorithm,ofport,members:port...) + bundleRe = regexp.MustCompile(`bundle\((\S+),(\S+),(\S+),ofport,members:(\S+)\)`) ) // TODO(mdlayher): replace parsing regex with arguments parsers @@ -187,6 +233,12 @@ func parseAction(s string) (Action, error) { return Normal(), nil case actionStripVLAN: return StripVLAN(), nil + case actionDecTTL, actionDecTTLNoParam: + return DecTTL(), nil + case actionCTClear: + return CTClear(), nil + case actionController: + return Controller(0, "", 0, "", false), nil } // ActionCT, with its arguments @@ -389,5 +441,111 @@ func parseAction(s string) (Action, error) { return SetField(ss[0][1], ss[0][2]), nil } + if ss := popRe.FindAllStringSubmatch(s, 1); len(ss) > 0 && len(ss[0]) == 2 { + // Results are: + // - full string + // - field + return Pop(ss[0][1]), nil + } + + if ss := pushRe.FindAllStringSubmatch(s, 1); len(ss) > 0 && len(ss[0]) == 2 { + // Results are: + // - full string + // - field + return Push(ss[0][1]), nil + } + + if ss := controllerMaxLenRe.FindAllStringSubmatch(s, 1); len(ss) > 0 && len(ss[0]) == 2 { + // Results are: + // - full string + // - maxLen + maxLen, err := strconv.Atoi(ss[0][1]) + if err != nil { + return nil, err + } + return Controller(maxLen, "", 0, "", false), nil + } + + if ss := controllerParamRe.FindAllStringSubmatch(s, 1); len(ss) > 0 && len(ss[0]) == 2 { + // Results are: + // - full string + // - options + var ( + maxLen int + id int + reason string + userdata string + pause bool + err error + ) + arr := strings.Split(ss[0][1], ",") + for _, item := range arr { + if item == "pause" { + pause = true + } else if strings.Contains(item, "=") { + kv := strings.Split(item, "=") + if len(kv) != 2 { + return nil, fmt.Errorf("invalid controller option: %s", item) + } + key := kv[0] + val := kv[1] + switch key { + case "maxLen": + maxLen, err = strconv.Atoi(val) + if err != nil { + return nil, err + } + case "id": + id, err = strconv.Atoi(val) + if err != nil { + return nil, err + } + case "reason": + reason = val + case "userdata": + userdata = val + } + } + } + return Controller(maxLen, reason, id, userdata, pause), nil + } + + if ss := groupRe.FindAllStringSubmatch(s, 1); len(ss) > 0 && len(ss[0]) == 2 { + // Results are: + // - full string + // - value + group, err := strconv.Atoi(ss[0][1]) + if err != nil { + return nil, err + } + return Group(group), nil + } + + if ss := bundleRe.FindAllStringSubmatch(s, 4); len(ss) > 0 && len(ss[0]) == 5 { + // Results are: + // - full string + // - fields + // - basis + // - algorithm + // - ports(comma sep) + fields := ss[0][1] + rawBasis := ss[0][2] + algorithm := ss[0][3] + rawPorts := ss[0][4] + basis, err := strconv.Atoi(rawBasis) + if err != nil { + return nil, err + } + var ports []int + for _, p := range strings.Split(rawPorts, ",") { + port, err := strconv.Atoi(p) + if err != nil { + return nil, err + } + ports = append(ports, port) + } + return Bundle(fields, basis, algorithm, ports...), nil + } + return nil, fmt.Errorf("no action matched for %q", s) }