cfm: Allow accurate transmission intervals in extended mode.
[sliver-openvswitch.git] / lib / cfm.c
1 /*
2  * Copyright (c) 2010, 2011 Nicira Networks.
3  *
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:
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 #include <config.h>
18 #include "cfm.h"
19
20 #include <assert.h>
21 #include <stdint.h>
22 #include <stdlib.h>
23 #include <string.h>
24
25 #include "dynamic-string.h"
26 #include "flow.h"
27 #include "hash.h"
28 #include "hmap.h"
29 #include "ofpbuf.h"
30 #include "packets.h"
31 #include "poll-loop.h"
32 #include "timer.h"
33 #include "timeval.h"
34 #include "unixctl.h"
35 #include "vlog.h"
36
37 VLOG_DEFINE_THIS_MODULE(cfm);
38
39 #define CFM_MAX_RMPS 256
40
41 /* Ethernet destination address of CCM packets. */
42 static const uint8_t eth_addr_ccm[6] = { 0x01, 0x80, 0xC2, 0x00, 0x00, 0x30 };
43 static const uint8_t eth_addr_ccm_x[6] = {
44     0x01, 0x23, 0x20, 0x00, 0x00, 0x30
45 };
46
47 #define ETH_TYPE_CFM 0x8902
48
49 /* A 'ccm' represents a Continuity Check Message from the 802.1ag
50  * specification.  Continuity Check Messages are broadcast periodically so that
51  * hosts can determine whom they have connectivity to. */
52 #define CCM_LEN 74
53 #define CCM_MAID_LEN 48
54 #define CCM_OPCODE 1 /* CFM message opcode meaning CCM. */
55 #define CCM_RDI_MASK 0x80
56 struct ccm {
57     uint8_t  mdlevel_version; /* MD Level and Version */
58     uint8_t  opcode;
59     uint8_t  flags;
60     uint8_t  tlv_offset;
61     ovs_be32 seq;
62     ovs_be16 mpid;
63     uint8_t  maid[CCM_MAID_LEN];
64
65     /* Defined by ITU-T Y.1731 should be zero */
66     ovs_be16 interval_ms_x;      /* Transmission interval in ms. */
67     uint8_t  zero[14];
68 } __attribute__((packed));
69 BUILD_ASSERT_DECL(CCM_LEN == sizeof(struct ccm));
70
71 struct cfm {
72     char *name;                 /* Name of this CFM object. */
73     struct hmap_node hmap_node; /* Node in all_cfms list. */
74
75     uint16_t mpid;
76     bool extended;         /* Extended mode. */
77     bool fault;            /* Indicates connectivity fault. */
78     bool unexpected_recv;  /* Received an unexpected CCM. */
79
80     uint32_t seq;          /* The sequence number of our last CCM. */
81     uint8_t ccm_interval;  /* The CCM transmission interval. */
82     int ccm_interval_ms;   /* 'ccm_interval' in milliseconds. */
83     uint8_t maid[CCM_MAID_LEN]; /* The MAID of this CFM. */
84
85     struct timer tx_timer;    /* Send CCM when expired. */
86     struct timer fault_timer; /* Check for faults when expired. */
87
88     struct hmap remote_mps;   /* Remote MPs. */
89 };
90
91 /* Remote MPs represent foreign network entities that are configured to have
92  * the same MAID as this CFM instance. */
93 struct remote_mp {
94     uint16_t mpid;         /* The Maintenance Point ID of this 'remote_mp'. */
95     struct hmap_node node; /* Node in 'remote_mps' map. */
96
97     bool recv;           /* CCM was received since last fault check. */
98     bool rdi;            /* Remote Defect Indicator. Indicates remote_mp isn't
99                             receiving CCMs that it's expecting to. */
100 };
101
102 static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
103 static struct hmap all_cfms = HMAP_INITIALIZER(&all_cfms);
104
105 static void cfm_unixctl_show(struct unixctl_conn *, const char *args,
106                              void *aux);
107
108 static const uint8_t *
109 cfm_ccm_addr(const struct cfm *cfm)
110 {
111     return cfm->extended ? eth_addr_ccm_x : eth_addr_ccm;
112 }
113
114 static void
115 cfm_generate_maid(struct cfm *cfm)
116 {
117     const char *ovs_md_name = "ovs";
118     const char *ovs_ma_name = "ovs";
119     uint8_t *ma_p;
120     size_t md_len, ma_len;
121
122     memset(cfm->maid, 0, CCM_MAID_LEN);
123
124     md_len = strlen(ovs_md_name);
125     ma_len = strlen(ovs_ma_name);
126
127     assert(md_len && ma_len && md_len + ma_len + 4 <= CCM_MAID_LEN);
128
129     cfm->maid[0] = 4;                           /* MD name string format. */
130     cfm->maid[1] = md_len;                      /* MD name size. */
131     memcpy(&cfm->maid[2], ovs_md_name, md_len); /* MD name. */
132
133     ma_p = cfm->maid + 2 + md_len;
134     ma_p[0] = 2;                           /* MA name string format. */
135     ma_p[1] = ma_len;                      /* MA name size. */
136     memcpy(&ma_p[2], ovs_ma_name, ma_len); /* MA name. */
137 }
138
139 static int
140 ccm_interval_to_ms(uint8_t interval)
141 {
142     switch (interval) {
143     case 0:  NOT_REACHED(); /* Explicitly not supported by 802.1ag. */
144     case 1:  return 3;      /* Not recommended due to timer resolution. */
145     case 2:  return 10;     /* Not recommended due to timer resolution. */
146     case 3:  return 100;
147     case 4:  return 1000;
148     case 5:  return 10000;
149     case 6:  return 60000;
150     case 7:  return 600000;
151     default: NOT_REACHED(); /* Explicitly not supported by 802.1ag. */
152     }
153
154     NOT_REACHED();
155 }
156
157 static long long int
158 cfm_fault_interval(struct cfm *cfm)
159 {
160     /* According to the 802.1ag specification we should assume every other MP
161      * with the same MAID has the same transmission interval that we have.  If
162      * an MP has a different interval, cfm_process_heartbeat will register it
163      * as a fault (likely due to a configuration error).  Thus we can check all
164      * MPs at once making this quite a bit simpler.
165      *
166      * According to the specification we should check when (ccm_interval_ms *
167      * 3.5)ms have passed. */
168     return (cfm->ccm_interval_ms * 7) / 2;
169 }
170
171 static uint8_t
172 ms_to_ccm_interval(int interval_ms)
173 {
174     uint8_t i;
175
176     for (i = 7; i > 0; i--) {
177         if (ccm_interval_to_ms(i) <= interval_ms) {
178             return i;
179         }
180     }
181
182     return 1;
183 }
184
185 static uint32_t
186 hash_mpid(uint8_t mpid)
187 {
188     return hash_int(mpid, 0);
189 }
190
191 static bool
192 cfm_is_valid_mpid(uint32_t mpid)
193 {
194     /* 802.1ag specification requires MPIDs to be within the range [1, 8191] */
195     return mpid >= 1 && mpid <= 8191;
196 }
197
198 static struct remote_mp *
199 lookup_remote_mp(const struct cfm *cfm, uint16_t mpid)
200 {
201     struct remote_mp *rmp;
202
203     HMAP_FOR_EACH_IN_BUCKET (rmp, node, hash_mpid(mpid), &cfm->remote_mps) {
204         if (rmp->mpid == mpid) {
205             return rmp;
206         }
207     }
208
209     return NULL;
210 }
211
212 void
213 cfm_init(void)
214 {
215     unixctl_command_register("cfm/show", cfm_unixctl_show, NULL);
216 }
217
218 /* Allocates a 'cfm' object called 'name'.  'cfm' should be initialized by
219  * cfm_configure() before use. */
220 struct cfm *
221 cfm_create(const char *name)
222 {
223     struct cfm *cfm;
224
225     cfm = xzalloc(sizeof *cfm);
226     cfm->name = xstrdup(name);
227     hmap_init(&cfm->remote_mps);
228     cfm_generate_maid(cfm);
229     hmap_insert(&all_cfms, &cfm->hmap_node, hash_string(cfm->name, 0));
230     return cfm;
231 }
232
233 void
234 cfm_destroy(struct cfm *cfm)
235 {
236     struct remote_mp *rmp, *rmp_next;
237
238     if (!cfm) {
239         return;
240     }
241
242     HMAP_FOR_EACH_SAFE (rmp, rmp_next, node, &cfm->remote_mps) {
243         hmap_remove(&cfm->remote_mps, &rmp->node);
244         free(rmp);
245     }
246
247     hmap_destroy(&cfm->remote_mps);
248     hmap_remove(&all_cfms, &cfm->hmap_node);
249     free(cfm->name);
250     free(cfm);
251 }
252
253 /* Should be run periodically to update fault statistics messages. */
254 void
255 cfm_run(struct cfm *cfm)
256 {
257     if (timer_expired(&cfm->fault_timer)) {
258         long long int interval = cfm_fault_interval(cfm);
259         struct remote_mp *rmp, *rmp_next;
260
261         cfm->fault = cfm->unexpected_recv;
262         cfm->unexpected_recv = false;
263
264         HMAP_FOR_EACH_SAFE (rmp, rmp_next, node, &cfm->remote_mps) {
265
266             if (!rmp->recv) {
267                 VLOG_DBG("%s: No CCM from RMP %"PRIu16" in the last %lldms",
268                          cfm->name, rmp->mpid, interval);
269                 hmap_remove(&cfm->remote_mps, &rmp->node);
270                 free(rmp);
271             } else {
272                 rmp->recv = false;
273
274                 if (rmp->mpid == cfm->mpid) {
275                     VLOG_WARN_RL(&rl, "%s: Received CCM with local MPID (%d)",
276                                  cfm->name, rmp->mpid);
277                     cfm->fault = true;
278                 }
279
280                 if (rmp->rdi) {
281                     VLOG_DBG("%s: RDI bit flagged from RMP %"PRIu16, cfm->name,
282                              rmp->mpid);
283                     cfm->fault = true;
284                 }
285             }
286         }
287
288         if (hmap_is_empty(&cfm->remote_mps)) {
289             cfm->fault = true;
290         }
291
292         timer_set_duration(&cfm->fault_timer, interval);
293     }
294 }
295
296 /* Should be run periodically to check if the CFM module has a CCM message it
297  * wishes to send. */
298 bool
299 cfm_should_send_ccm(struct cfm *cfm)
300 {
301     return timer_expired(&cfm->tx_timer);
302 }
303
304 /* Composes a CCM message into 'packet'.  Messages generated with this function
305  * should be sent whenever cfm_should_send_ccm() indicates. */
306 void
307 cfm_compose_ccm(struct cfm *cfm, struct ofpbuf *packet,
308                 uint8_t eth_src[ETH_ADDR_LEN])
309 {
310     struct ccm *ccm;
311
312     timer_set_duration(&cfm->tx_timer, cfm->ccm_interval_ms);
313     ccm = eth_compose(packet, cfm_ccm_addr(cfm), eth_src, ETH_TYPE_CFM,
314                       sizeof *ccm);
315     ccm->mdlevel_version = 0;
316     ccm->opcode = CCM_OPCODE;
317     ccm->tlv_offset = 70;
318     ccm->seq = htonl(++cfm->seq);
319     ccm->mpid = htons(cfm->mpid);
320     ccm->flags = cfm->ccm_interval;
321     memcpy(ccm->maid, cfm->maid, sizeof ccm->maid);
322     memset(ccm->zero, 0, sizeof ccm->zero);
323
324     if (cfm->ccm_interval == 0) {
325         assert(cfm->extended);
326         ccm->interval_ms_x = htons(cfm->ccm_interval_ms);
327     }
328
329     if (hmap_is_empty(&cfm->remote_mps)) {
330         ccm->flags |= CCM_RDI_MASK;
331     }
332 }
333
334 void
335 cfm_wait(struct cfm *cfm)
336 {
337
338     timer_wait(&cfm->tx_timer);
339     timer_wait(&cfm->fault_timer);
340 }
341
342 /* Configures 'cfm' with settings from 's'. */
343 bool
344 cfm_configure(struct cfm *cfm, const struct cfm_settings *s)
345 {
346     uint8_t interval;
347     int interval_ms;
348
349     if (!cfm_is_valid_mpid(s->mpid) || s->interval <= 0) {
350         return false;
351     }
352
353     cfm->mpid = s->mpid;
354     cfm->extended = s->extended;
355     interval = ms_to_ccm_interval(s->interval);
356     interval_ms = ccm_interval_to_ms(interval);
357
358     if (cfm->extended && interval_ms != s->interval) {
359         interval = 0;
360         interval_ms = MIN(s->interval, UINT16_MAX);
361     }
362
363     if (interval != cfm->ccm_interval || interval_ms != cfm->ccm_interval_ms) {
364         cfm->ccm_interval = interval;
365         cfm->ccm_interval_ms = interval_ms;
366
367         timer_set_expired(&cfm->tx_timer);
368         timer_set_duration(&cfm->fault_timer, cfm_fault_interval(cfm));
369     }
370
371     return true;
372 }
373
374 /* Returns true if 'cfm' should process packets from 'flow'. */
375 bool
376 cfm_should_process_flow(const struct cfm *cfm, const struct flow *flow)
377 {
378     return (ntohs(flow->dl_type) == ETH_TYPE_CFM
379             && eth_addr_equals(flow->dl_dst, cfm_ccm_addr(cfm)));
380 }
381
382 /* Updates internal statistics relevant to packet 'p'.  Should be called on
383  * every packet whose flow returned true when passed to
384  * cfm_should_process_flow. */
385 void
386 cfm_process_heartbeat(struct cfm *cfm, const struct ofpbuf *p)
387 {
388     struct ccm *ccm;
389     struct eth_header *eth;
390
391     eth = p->l2;
392     ccm = ofpbuf_at(p, (uint8_t *)p->l3 - (uint8_t *)p->data, CCM_LEN);
393
394     if (!ccm) {
395         VLOG_INFO_RL(&rl, "%s: Received an unparseable 802.1ag CCM heartbeat.",
396                      cfm->name);
397         return;
398     }
399
400     if (ccm->opcode != CCM_OPCODE) {
401         VLOG_INFO_RL(&rl, "%s: Received an unsupported 802.1ag message. "
402                      "(opcode %u)", cfm->name, ccm->opcode);
403         return;
404     }
405
406     /* According to the 802.1ag specification, reception of a CCM with an
407      * incorrect ccm_interval, unexpected MAID, or unexpected MPID should
408      * trigger a fault.  We ignore this requirement for several reasons.
409      *
410      * Faults can cause a controller or Open vSwitch to make potentially
411      * expensive changes to the network topology.  It seems prudent to trigger
412      * them judiciously, especially when CFM is used to check slave status of
413      * bonds. Furthermore, faults can be maliciously triggered by crafting
414      * invalid CCMs. */
415     if (memcmp(ccm->maid, cfm->maid, sizeof ccm->maid)) {
416         cfm->unexpected_recv = true;
417         VLOG_WARN_RL(&rl, "%s: Received unexpected remote MAID from MAC "
418                      ETH_ADDR_FMT, cfm->name, ETH_ADDR_ARGS(eth->eth_src));
419     } else {
420         uint16_t ccm_mpid = ntohs(ccm->mpid);
421         uint8_t ccm_interval = ccm->flags & 0x7;
422         bool ccm_rdi = ccm->flags & CCM_RDI_MASK;
423         uint16_t ccm_interval_ms_x = ntohs(ccm->interval_ms_x);
424
425         struct remote_mp *rmp;
426
427         if (ccm_interval != cfm->ccm_interval) {
428             VLOG_WARN_RL(&rl, "%s: received a CCM with an invalid interval"
429                          " (%"PRIu8") from RMP %"PRIu16, cfm->name,
430                          ccm_interval, ccm_mpid);
431         }
432
433         if (cfm->extended && ccm_interval == 0
434             && ccm_interval_ms_x != cfm->ccm_interval_ms) {
435             VLOG_WARN_RL(&rl, "%s: received a CCM with an invalid extended"
436                          " interval (%"PRIu16"ms) from RMP %"PRIu16, cfm->name,
437                          ccm_interval_ms_x, ccm_mpid);
438         }
439
440         rmp = lookup_remote_mp(cfm, ccm_mpid);
441         if (rmp) {
442             rmp->recv = true;
443             rmp->rdi = ccm_rdi;
444         } else if (hmap_count(&cfm->remote_mps) < CFM_MAX_RMPS) {
445             rmp = xmalloc(sizeof *rmp);
446             rmp->mpid = ccm_mpid;
447             rmp->recv = true;
448             rmp->rdi = ccm_rdi;
449             hmap_insert(&cfm->remote_mps, &rmp->node, hash_mpid(rmp->mpid));
450         } else {
451             cfm->unexpected_recv = true;
452             VLOG_WARN_RL(&rl, "%s: dropped CCM with MPID %d from MAC "
453                          ETH_ADDR_FMT, cfm->name, ccm_mpid,
454                          ETH_ADDR_ARGS(eth->eth_src));
455         }
456
457         VLOG_DBG("%s: Received CCM (seq %"PRIu32") (mpid %"PRIu16")"
458                  " (interval %"PRIu8") (RDI %s)", cfm->name, ntohl(ccm->seq),
459                  ccm_mpid, ccm_interval, ccm_rdi ? "true" : "false");
460     }
461 }
462
463 /* Gets the fault status of 'cfm'.  Returns true when 'cfm' has detected
464  * connectivity problems, false otherwise. */
465 bool
466 cfm_get_fault(const struct cfm *cfm)
467 {
468     return cfm->fault;
469 }
470
471 static struct cfm *
472 cfm_find(const char *name)
473 {
474     struct cfm *cfm;
475
476     HMAP_FOR_EACH_WITH_HASH (cfm, hmap_node, hash_string(name, 0), &all_cfms) {
477         if (!strcmp(cfm->name, name)) {
478             return cfm;
479         }
480     }
481     return NULL;
482 }
483
484 static void
485 cfm_unixctl_show(struct unixctl_conn *conn,
486                  const char *args, void *aux OVS_UNUSED)
487 {
488     struct ds ds = DS_EMPTY_INITIALIZER;
489     const struct cfm *cfm;
490     struct remote_mp *rmp;
491
492     cfm = cfm_find(args);
493     if (!cfm) {
494         unixctl_command_reply(conn, 501, "no such CFM object");
495         return;
496     }
497
498     ds_put_format(&ds, "MPID %"PRIu16":%s%s\n", cfm->mpid,
499                   cfm->fault ? " fault" : "",
500                   cfm->unexpected_recv ? " unexpected_recv" : "");
501
502     ds_put_format(&ds, "\tinterval: %dms\n", cfm->ccm_interval_ms);
503     ds_put_format(&ds, "\tnext CCM tx: %lldms\n",
504                   timer_msecs_until_expired(&cfm->tx_timer));
505     ds_put_format(&ds, "\tnext fault check: %lldms\n",
506                   timer_msecs_until_expired(&cfm->fault_timer));
507
508     ds_put_cstr(&ds, "\n");
509     HMAP_FOR_EACH (rmp, node, &cfm->remote_mps) {
510         ds_put_format(&ds, "Remote MPID %"PRIu16":%s\n", rmp->mpid,
511                       rmp->rdi ? " rdi" : "");
512         ds_put_format(&ds, "\trecv since check: %s",
513                       rmp->recv ? "true" : "false");
514     }
515
516     unixctl_command_reply(conn, 200, ds_cstr(&ds));
517     ds_destroy(&ds);
518 }