2 * Copyright (c) 2011, 2012, 2013, 2014 Nicira, Inc.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at:
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
21 #include "byte-order.h"
22 #include "dynamic-string.h"
24 #include "meta-flow.h"
26 #include "ofp-actions.h"
27 #include "ofp-errors.h"
30 #include "openflow/openflow.h"
31 #include "unaligned.h"
34 get_be16(const void **pp
)
36 const ovs_be16
*p
= *pp
;
43 get_be32(const void **pp
)
45 const ovs_be32
*p
= *pp
;
46 ovs_be32 value
= get_unaligned_be32(p
);
52 get_subfield(int n_bits
, const void **p
, struct mf_subfield
*sf
)
54 sf
->field
= mf_from_nxm_header(ntohl(get_be32(p
)));
55 sf
->ofs
= ntohs(get_be16(p
));
60 learn_min_len(uint16_t header
)
62 int n_bits
= header
& NX_LEARN_N_BITS_MASK
;
63 int src_type
= header
& NX_LEARN_SRC_MASK
;
64 int dst_type
= header
& NX_LEARN_DST_MASK
;
68 if (src_type
== NX_LEARN_SRC_FIELD
) {
69 min_len
+= sizeof(ovs_be32
); /* src_field */
70 min_len
+= sizeof(ovs_be16
); /* src_ofs */
72 min_len
+= DIV_ROUND_UP(n_bits
, 16);
74 if (dst_type
== NX_LEARN_DST_MATCH
||
75 dst_type
== NX_LEARN_DST_LOAD
) {
76 min_len
+= sizeof(ovs_be32
); /* dst_field */
77 min_len
+= sizeof(ovs_be16
); /* dst_ofs */
82 /* Converts 'nal' into a "struct ofpact_learn" and appends that struct to
83 * 'ofpacts'. Returns 0 if successful, otherwise an OFPERR_*. */
85 learn_from_openflow(const struct nx_action_learn
*nal
, struct ofpbuf
*ofpacts
)
87 struct ofpact_learn
*learn
;
91 return OFPERR_OFPBAC_BAD_ARGUMENT
;
94 learn
= ofpact_put_LEARN(ofpacts
);
96 learn
->idle_timeout
= ntohs(nal
->idle_timeout
);
97 learn
->hard_timeout
= ntohs(nal
->hard_timeout
);
98 learn
->priority
= ntohs(nal
->priority
);
99 learn
->cookie
= ntohll(nal
->cookie
);
100 learn
->table_id
= nal
->table_id
;
101 learn
->fin_idle_timeout
= ntohs(nal
->fin_idle_timeout
);
102 learn
->fin_hard_timeout
= ntohs(nal
->fin_hard_timeout
);
104 /* We only support "send-flow-removed" for now. */
105 switch (ntohs(nal
->flags
)) {
109 case OFPFF_SEND_FLOW_REM
:
110 learn
->flags
= OFPUTIL_FF_SEND_FLOW_REM
;
113 return OFPERR_OFPBAC_BAD_ARGUMENT
;
116 if (learn
->table_id
== 0xff) {
117 return OFPERR_OFPBAC_BAD_ARGUMENT
;
120 end
= (char *) nal
+ ntohs(nal
->len
);
121 for (p
= nal
+ 1; p
!= end
; ) {
122 struct ofpact_learn_spec
*spec
;
123 uint16_t header
= ntohs(get_be16(&p
));
129 spec
= ofpbuf_put_zeros(ofpacts
, sizeof *spec
);
130 learn
= ofpacts
->frame
;
133 spec
->src_type
= header
& NX_LEARN_SRC_MASK
;
134 spec
->dst_type
= header
& NX_LEARN_DST_MASK
;
135 spec
->n_bits
= header
& NX_LEARN_N_BITS_MASK
;
137 /* Check for valid src and dst type combination. */
138 if (spec
->dst_type
== NX_LEARN_DST_MATCH
||
139 spec
->dst_type
== NX_LEARN_DST_LOAD
||
140 (spec
->dst_type
== NX_LEARN_DST_OUTPUT
&&
141 spec
->src_type
== NX_LEARN_SRC_FIELD
)) {
144 return OFPERR_OFPBAC_BAD_ARGUMENT
;
147 /* Check that the arguments don't overrun the end of the action. */
148 if ((char *) end
- (char *) p
< learn_min_len(header
)) {
149 return OFPERR_OFPBAC_BAD_LEN
;
152 /* Get the source. */
153 if (spec
->src_type
== NX_LEARN_SRC_FIELD
) {
154 get_subfield(spec
->n_bits
, &p
, &spec
->src
);
156 int p_bytes
= 2 * DIV_ROUND_UP(spec
->n_bits
, 16);
158 bitwise_copy(p
, p_bytes
, 0,
159 &spec
->src_imm
, sizeof spec
->src_imm
, 0,
161 p
= (const uint8_t *) p
+ p_bytes
;
164 /* Get the destination. */
165 if (spec
->dst_type
== NX_LEARN_DST_MATCH
||
166 spec
->dst_type
== NX_LEARN_DST_LOAD
) {
167 get_subfield(spec
->n_bits
, &p
, &spec
->dst
);
170 ofpact_update_len(ofpacts
, &learn
->ofpact
);
172 if (!is_all_zeros(p
, (char *) end
- (char *) p
)) {
173 return OFPERR_OFPBAC_BAD_ARGUMENT
;
179 /* Checks that 'learn' is a valid action on 'flow'. Returns 0 if it is valid,
180 * otherwise an OFPERR_*. */
182 learn_check(const struct ofpact_learn
*learn
, const struct flow
*flow
)
184 const struct ofpact_learn_spec
*spec
;
187 match_init_catchall(&match
);
188 for (spec
= learn
->specs
; spec
< &learn
->specs
[learn
->n_specs
]; spec
++) {
191 /* Check the source. */
192 if (spec
->src_type
== NX_LEARN_SRC_FIELD
) {
193 error
= mf_check_src(&spec
->src
, flow
);
199 /* Check the destination. */
200 switch (spec
->dst_type
) {
201 case NX_LEARN_DST_MATCH
:
202 error
= mf_check_src(&spec
->dst
, &match
.flow
);
207 mf_write_subfield(&spec
->dst
, &spec
->src_imm
, &match
);
210 case NX_LEARN_DST_LOAD
:
211 error
= mf_check_dst(&spec
->dst
, &match
.flow
);
217 case NX_LEARN_DST_OUTPUT
:
226 put_be16(struct ofpbuf
*b
, ovs_be16 x
)
228 ofpbuf_put(b
, &x
, sizeof x
);
232 put_be32(struct ofpbuf
*b
, ovs_be32 x
)
234 ofpbuf_put(b
, &x
, sizeof x
);
238 put_u16(struct ofpbuf
*b
, uint16_t x
)
240 put_be16(b
, htons(x
));
244 put_u32(struct ofpbuf
*b
, uint32_t x
)
246 put_be32(b
, htonl(x
));
249 /* Converts 'learn' into a "struct nx_action_learn" and appends that action to
252 learn_to_nxast(const struct ofpact_learn
*learn
, struct ofpbuf
*openflow
)
254 const struct ofpact_learn_spec
*spec
;
255 struct nx_action_learn
*nal
;
258 start_ofs
= ofpbuf_size(openflow
);
259 nal
= ofputil_put_NXAST_LEARN(openflow
);
260 nal
->idle_timeout
= htons(learn
->idle_timeout
);
261 nal
->hard_timeout
= htons(learn
->hard_timeout
);
262 nal
->fin_idle_timeout
= htons(learn
->fin_idle_timeout
);
263 nal
->fin_hard_timeout
= htons(learn
->fin_hard_timeout
);
264 nal
->priority
= htons(learn
->priority
);
265 nal
->cookie
= htonll(learn
->cookie
);
266 nal
->flags
= htons(learn
->flags
);
267 nal
->table_id
= learn
->table_id
;
269 for (spec
= learn
->specs
; spec
< &learn
->specs
[learn
->n_specs
]; spec
++) {
270 put_u16(openflow
, spec
->n_bits
| spec
->dst_type
| spec
->src_type
);
272 if (spec
->src_type
== NX_LEARN_SRC_FIELD
) {
273 put_u32(openflow
, spec
->src
.field
->nxm_header
);
274 put_u16(openflow
, spec
->src
.ofs
);
276 size_t n_dst_bytes
= 2 * DIV_ROUND_UP(spec
->n_bits
, 16);
277 uint8_t *bits
= ofpbuf_put_zeros(openflow
, n_dst_bytes
);
278 bitwise_copy(&spec
->src_imm
, sizeof spec
->src_imm
, 0,
279 bits
, n_dst_bytes
, 0,
283 if (spec
->dst_type
== NX_LEARN_DST_MATCH
||
284 spec
->dst_type
== NX_LEARN_DST_LOAD
) {
285 put_u32(openflow
, spec
->dst
.field
->nxm_header
);
286 put_u16(openflow
, spec
->dst
.ofs
);
290 if ((ofpbuf_size(openflow
) - start_ofs
) % 8) {
291 ofpbuf_put_zeros(openflow
, 8 - (ofpbuf_size(openflow
) - start_ofs
) % 8);
294 nal
= ofpbuf_at_assert(openflow
, start_ofs
, sizeof *nal
);
295 nal
->len
= htons(ofpbuf_size(openflow
) - start_ofs
);
298 /* Composes 'fm' so that executing it will implement 'learn' given that the
299 * packet being processed has 'flow' as its flow.
301 * Uses 'ofpacts' to store the flow mod's actions. The caller must initialize
302 * 'ofpacts' and retains ownership of it. 'fm->ofpacts' will point into the
305 * The caller has to actually execute 'fm'. */
307 learn_execute(const struct ofpact_learn
*learn
, const struct flow
*flow
,
308 struct ofputil_flow_mod
*fm
, struct ofpbuf
*ofpacts
)
310 const struct ofpact_learn_spec
*spec
;
312 match_init_catchall(&fm
->match
);
313 fm
->priority
= learn
->priority
;
314 fm
->cookie
= htonll(0);
315 fm
->cookie_mask
= htonll(0);
316 fm
->new_cookie
= htonll(learn
->cookie
);
317 fm
->modify_cookie
= fm
->new_cookie
!= OVS_BE64_MAX
;
318 fm
->table_id
= learn
->table_id
;
319 fm
->command
= OFPFC_MODIFY_STRICT
;
320 fm
->idle_timeout
= learn
->idle_timeout
;
321 fm
->hard_timeout
= learn
->hard_timeout
;
322 fm
->buffer_id
= UINT32_MAX
;
323 fm
->out_port
= OFPP_NONE
;
324 fm
->flags
= learn
->flags
;
328 if (learn
->fin_idle_timeout
|| learn
->fin_hard_timeout
) {
329 struct ofpact_fin_timeout
*oft
;
331 oft
= ofpact_put_FIN_TIMEOUT(ofpacts
);
332 oft
->fin_idle_timeout
= learn
->fin_idle_timeout
;
333 oft
->fin_hard_timeout
= learn
->fin_hard_timeout
;
336 for (spec
= learn
->specs
; spec
< &learn
->specs
[learn
->n_specs
]; spec
++) {
337 union mf_subvalue value
;
340 if (spec
->src_type
== NX_LEARN_SRC_FIELD
) {
341 mf_read_subfield(&spec
->src
, flow
, &value
);
343 value
= spec
->src_imm
;
346 switch (spec
->dst_type
) {
347 case NX_LEARN_DST_MATCH
:
348 mf_write_subfield(&spec
->dst
, &value
, &fm
->match
);
351 case NX_LEARN_DST_LOAD
:
352 for (ofs
= 0; ofs
< spec
->n_bits
; ofs
+= chunk
) {
353 struct ofpact_reg_load
*load
;
355 chunk
= MIN(spec
->n_bits
- ofs
, 64);
357 load
= ofpact_put_REG_LOAD(ofpacts
);
358 load
->dst
.field
= spec
->dst
.field
;
359 load
->dst
.ofs
= spec
->dst
.ofs
+ ofs
;
360 load
->dst
.n_bits
= chunk
;
361 bitwise_copy(&value
, sizeof value
, ofs
,
362 &load
->subvalue
, sizeof load
->subvalue
, 0,
367 case NX_LEARN_DST_OUTPUT
:
368 if (spec
->n_bits
<= 16
369 || is_all_zeros(value
.u8
, sizeof value
- 2)) {
370 ofp_port_t port
= u16_to_ofp(ntohs(value
.be16
[7]));
372 if (ofp_to_u16(port
) < ofp_to_u16(OFPP_MAX
)
373 || port
== OFPP_IN_PORT
374 || port
== OFPP_FLOOD
375 || port
== OFPP_LOCAL
376 || port
== OFPP_ALL
) {
377 ofpact_put_OUTPUT(ofpacts
)->port
= port
;
385 fm
->ofpacts
= ofpbuf_data(ofpacts
);
386 fm
->ofpacts_len
= ofpbuf_size(ofpacts
);
389 /* Perform a bitwise-OR on 'wc''s fields that are relevant as sources in
390 * the learn action 'learn'. */
392 learn_mask(const struct ofpact_learn
*learn
, struct flow_wildcards
*wc
)
394 const struct ofpact_learn_spec
*spec
;
395 union mf_subvalue value
;
397 memset(&value
, 0xff, sizeof value
);
398 for (spec
= learn
->specs
; spec
< &learn
->specs
[learn
->n_specs
]; spec
++) {
399 if (spec
->src_type
== NX_LEARN_SRC_FIELD
) {
400 mf_write_subfield_flow(&spec
->src
, &value
, &wc
->masks
);
405 /* Returns NULL if successful, otherwise a malloc()'d string describing the
406 * error. The caller is responsible for freeing the returned string. */
407 static char * WARN_UNUSED_RESULT
408 learn_parse_load_immediate(const char *s
, struct ofpact_learn_spec
*spec
)
410 const char *full_s
= s
;
411 const char *arrow
= strstr(s
, "->");
412 struct mf_subfield dst
;
413 union mf_subvalue imm
;
416 memset(&imm
, 0, sizeof imm
);
417 if (s
[0] == '0' && (s
[1] == 'x' || s
[1] == 'X') && arrow
) {
418 const char *in
= arrow
- 1;
419 uint8_t *out
= imm
.u8
+ sizeof imm
.u8
- 1;
420 int n
= arrow
- (s
+ 2);
423 for (i
= 0; i
< n
; i
++) {
424 int hexit
= hexit_value(in
[-i
]);
426 return xasprintf("%s: bad hex digit in value", full_s
);
428 out
[-(i
/ 2)] |= i
% 2 ? hexit
<< 4 : hexit
;
432 imm
.be64
[1] = htonll(strtoull(s
, (char **) &s
, 0));
435 if (strncmp(s
, "->", 2)) {
436 return xasprintf("%s: missing `->' following value", full_s
);
440 error
= mf_parse_subfield(&dst
, s
);
445 if (!bitwise_is_all_zeros(&imm
, sizeof imm
, dst
.n_bits
,
446 (8 * sizeof imm
) - dst
.n_bits
)) {
447 return xasprintf("%s: value does not fit into %u bits",
451 spec
->n_bits
= dst
.n_bits
;
452 spec
->src_type
= NX_LEARN_SRC_IMMEDIATE
;
454 spec
->dst_type
= NX_LEARN_DST_LOAD
;
459 /* Returns NULL if successful, otherwise a malloc()'d string describing the
460 * error. The caller is responsible for freeing the returned string. */
461 static char * WARN_UNUSED_RESULT
462 learn_parse_spec(const char *orig
, char *name
, char *value
,
463 struct ofpact_learn_spec
*spec
)
465 if (mf_from_name(name
)) {
466 const struct mf_field
*dst
= mf_from_name(name
);
470 error
= mf_parse_value(dst
, value
, &imm
);
475 spec
->n_bits
= dst
->n_bits
;
476 spec
->src_type
= NX_LEARN_SRC_IMMEDIATE
;
477 memset(&spec
->src_imm
, 0, sizeof spec
->src_imm
);
478 memcpy(&spec
->src_imm
.u8
[sizeof spec
->src_imm
- dst
->n_bytes
],
480 spec
->dst_type
= NX_LEARN_DST_MATCH
;
481 spec
->dst
.field
= dst
;
483 spec
->dst
.n_bits
= dst
->n_bits
;
484 } else if (strchr(name
, '[')) {
485 /* Parse destination and check prerequisites. */
488 error
= mf_parse_subfield(&spec
->dst
, name
);
493 /* Parse source and check prerequisites. */
494 if (value
[0] != '\0') {
495 error
= mf_parse_subfield(&spec
->src
, value
);
499 if (spec
->src
.n_bits
!= spec
->dst
.n_bits
) {
500 return xasprintf("%s: bit widths of %s (%u) and %s (%u) "
501 "differ", orig
, name
, spec
->src
.n_bits
, value
,
505 spec
->src
= spec
->dst
;
508 spec
->n_bits
= spec
->src
.n_bits
;
509 spec
->src_type
= NX_LEARN_SRC_FIELD
;
510 spec
->dst_type
= NX_LEARN_DST_MATCH
;
511 } else if (!strcmp(name
, "load")) {
512 if (value
[strcspn(value
, "[-")] == '-') {
513 char *error
= learn_parse_load_immediate(value
, spec
);
518 struct ofpact_reg_move move
;
521 error
= nxm_parse_reg_move(&move
, value
);
526 spec
->n_bits
= move
.src
.n_bits
;
527 spec
->src_type
= NX_LEARN_SRC_FIELD
;
528 spec
->src
= move
.src
;
529 spec
->dst_type
= NX_LEARN_DST_LOAD
;
530 spec
->dst
= move
.dst
;
532 } else if (!strcmp(name
, "output")) {
533 char *error
= mf_parse_subfield(&spec
->src
, value
);
538 spec
->n_bits
= spec
->src
.n_bits
;
539 spec
->src_type
= NX_LEARN_SRC_FIELD
;
540 spec
->dst_type
= NX_LEARN_DST_OUTPUT
;
542 return xasprintf("%s: unknown keyword %s", orig
, name
);
548 /* Returns NULL if successful, otherwise a malloc()'d string describing the
549 * error. The caller is responsible for freeing the returned string. */
550 static char * WARN_UNUSED_RESULT
551 learn_parse__(char *orig
, char *arg
, struct ofpbuf
*ofpacts
)
553 struct ofpact_learn
*learn
;
557 learn
= ofpact_put_LEARN(ofpacts
);
558 learn
->idle_timeout
= OFP_FLOW_PERMANENT
;
559 learn
->hard_timeout
= OFP_FLOW_PERMANENT
;
560 learn
->priority
= OFP_DEFAULT_PRIORITY
;
563 match_init_catchall(&match
);
564 while (ofputil_parse_key_value(&arg
, &name
, &value
)) {
565 if (!strcmp(name
, "table")) {
566 learn
->table_id
= atoi(value
);
567 if (learn
->table_id
== 255) {
568 return xasprintf("%s: table id 255 not valid for `learn' "
571 } else if (!strcmp(name
, "priority")) {
572 learn
->priority
= atoi(value
);
573 } else if (!strcmp(name
, "idle_timeout")) {
574 learn
->idle_timeout
= atoi(value
);
575 } else if (!strcmp(name
, "hard_timeout")) {
576 learn
->hard_timeout
= atoi(value
);
577 } else if (!strcmp(name
, "fin_idle_timeout")) {
578 learn
->fin_idle_timeout
= atoi(value
);
579 } else if (!strcmp(name
, "fin_hard_timeout")) {
580 learn
->fin_hard_timeout
= atoi(value
);
581 } else if (!strcmp(name
, "cookie")) {
582 learn
->cookie
= strtoull(value
, NULL
, 0);
583 } else if (!strcmp(name
, "send_flow_rem")) {
584 learn
->flags
|= OFPFF_SEND_FLOW_REM
;
586 struct ofpact_learn_spec
*spec
;
589 spec
= ofpbuf_put_zeros(ofpacts
, sizeof *spec
);
590 learn
= ofpacts
->frame
;
593 error
= learn_parse_spec(orig
, name
, value
, spec
);
598 /* Update 'match' to allow for satisfying destination
600 if (spec
->src_type
== NX_LEARN_SRC_IMMEDIATE
601 && spec
->dst_type
== NX_LEARN_DST_MATCH
) {
602 mf_write_subfield(&spec
->dst
, &spec
->src_imm
, &match
);
606 ofpact_update_len(ofpacts
, &learn
->ofpact
);
611 /* Parses 'arg' as a set of arguments to the "learn" action and appends a
612 * matching OFPACT_LEARN action to 'ofpacts'. ovs-ofctl(8) describes the
615 * Returns NULL if successful, otherwise a malloc()'d string describing the
616 * error. The caller is responsible for freeing the returned string.
618 * If 'flow' is nonnull, then it should be the flow from a struct match that is
619 * the matching rule for the learning action. This helps to better validate
620 * the action's arguments.
623 char * WARN_UNUSED_RESULT
624 learn_parse(char *arg
, struct ofpbuf
*ofpacts
)
626 char *orig
= xstrdup(arg
);
627 char *error
= learn_parse__(orig
, arg
, ofpacts
);
632 /* Appends a description of 'learn' to 's', in the format that ovs-ofctl(8)
635 learn_format(const struct ofpact_learn
*learn
, struct ds
*s
)
637 const struct ofpact_learn_spec
*spec
;
640 match_init_catchall(&match
);
642 ds_put_format(s
, "learn(table=%"PRIu8
, learn
->table_id
);
643 if (learn
->idle_timeout
!= OFP_FLOW_PERMANENT
) {
644 ds_put_format(s
, ",idle_timeout=%"PRIu16
, learn
->idle_timeout
);
646 if (learn
->hard_timeout
!= OFP_FLOW_PERMANENT
) {
647 ds_put_format(s
, ",hard_timeout=%"PRIu16
, learn
->hard_timeout
);
649 if (learn
->fin_idle_timeout
) {
650 ds_put_format(s
, ",fin_idle_timeout=%"PRIu16
, learn
->fin_idle_timeout
);
652 if (learn
->fin_hard_timeout
) {
653 ds_put_format(s
, ",fin_hard_timeout=%"PRIu16
, learn
->fin_hard_timeout
);
655 if (learn
->priority
!= OFP_DEFAULT_PRIORITY
) {
656 ds_put_format(s
, ",priority=%"PRIu16
, learn
->priority
);
658 if (learn
->flags
& OFPFF_SEND_FLOW_REM
) {
659 ds_put_cstr(s
, ",send_flow_rem");
661 if (learn
->cookie
!= 0) {
662 ds_put_format(s
, ",cookie=%#"PRIx64
, learn
->cookie
);
665 for (spec
= learn
->specs
; spec
< &learn
->specs
[learn
->n_specs
]; spec
++) {
668 switch (spec
->src_type
| spec
->dst_type
) {
669 case NX_LEARN_SRC_IMMEDIATE
| NX_LEARN_DST_MATCH
:
670 if (spec
->dst
.ofs
== 0
671 && spec
->dst
.n_bits
== spec
->dst
.field
->n_bits
) {
672 union mf_value value
;
674 memset(&value
, 0, sizeof value
);
675 bitwise_copy(&spec
->src_imm
, sizeof spec
->src_imm
, 0,
676 &value
, spec
->dst
.field
->n_bytes
, 0,
677 spec
->dst
.field
->n_bits
);
678 ds_put_format(s
, "%s=", spec
->dst
.field
->name
);
679 mf_format(spec
->dst
.field
, &value
, NULL
, s
);
681 mf_format_subfield(&spec
->dst
, s
);
683 mf_format_subvalue(&spec
->src_imm
, s
);
687 case NX_LEARN_SRC_FIELD
| NX_LEARN_DST_MATCH
:
688 mf_format_subfield(&spec
->dst
, s
);
689 if (spec
->src
.field
!= spec
->dst
.field
||
690 spec
->src
.ofs
!= spec
->dst
.ofs
) {
692 mf_format_subfield(&spec
->src
, s
);
696 case NX_LEARN_SRC_IMMEDIATE
| NX_LEARN_DST_LOAD
:
697 ds_put_format(s
, "load:");
698 mf_format_subvalue(&spec
->src_imm
, s
);
699 ds_put_cstr(s
, "->");
700 mf_format_subfield(&spec
->dst
, s
);
703 case NX_LEARN_SRC_FIELD
| NX_LEARN_DST_LOAD
:
704 ds_put_cstr(s
, "load:");
705 mf_format_subfield(&spec
->src
, s
);
706 ds_put_cstr(s
, "->");
707 mf_format_subfield(&spec
->dst
, s
);
710 case NX_LEARN_SRC_FIELD
| NX_LEARN_DST_OUTPUT
:
711 ds_put_cstr(s
, "output:");
712 mf_format_subfield(&spec
->src
, s
);