d927b51cb4
As a consequence, this fixes tests for multicast and local Ethernet addresses, which formerly tested for the high bits but should have tested for the low bits of the first octet.
656 lines
19 KiB
C
656 lines
19 KiB
C
/* Copyright (c) 2008 The Board of Trustees of The Leland Stanford
|
|
* Junior University
|
|
*
|
|
* We are making the OpenFlow specification and associated documentation
|
|
* (Software) available for public use and benefit with the expectation
|
|
* that others will use, modify and enhance the Software and contribute
|
|
* those enhancements back to the community. However, since we would
|
|
* like to make the Software available for broadest use, with as few
|
|
* restrictions as possible permission is hereby granted, free of
|
|
* charge, to any person obtaining a copy of this Software to deal in
|
|
* the Software under the copyrights without restriction, including
|
|
* without limitation the rights to use, copy, modify, merge, publish,
|
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
|
* permit persons to whom the Software is furnished to do so, subject to
|
|
* the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*
|
|
* The name and trademarks of copyright holder(s) may NOT be used in
|
|
* advertising or publicity pertaining to the Software or any
|
|
* derivatives without specific, written prior permission.
|
|
*/
|
|
|
|
#include <assert.h>
|
|
#include <errno.h>
|
|
#include <getopt.h>
|
|
#include <inttypes.h>
|
|
#include <netinet/in.h>
|
|
#include <poll.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include "buffer.h"
|
|
#include "command-line.h"
|
|
#include "compiler.h"
|
|
#include "fault.h"
|
|
#include "flow.h"
|
|
#include "hash.h"
|
|
#include "list.h"
|
|
#include "ofp-print.h"
|
|
#include "openflow.h"
|
|
#include "packets.h"
|
|
#include "poll-loop.h"
|
|
#include "queue.h"
|
|
#include "time.h"
|
|
#include "util.h"
|
|
#include "vconn-ssl.h"
|
|
#include "vconn.h"
|
|
#include "vlog-socket.h"
|
|
#include "xtoxll.h"
|
|
|
|
#include "vlog.h"
|
|
#define THIS_MODULE VLM_controller
|
|
|
|
#define MAX_SWITCHES 16
|
|
#define MAX_TXQ 128
|
|
|
|
struct switch_ {
|
|
char *name;
|
|
struct vconn *vconn;
|
|
|
|
uint64_t datapath_id;
|
|
time_t last_control_hello;
|
|
|
|
struct queue txq;
|
|
};
|
|
|
|
/* -H, --hub: Use dumb hub instead of learning switch? */
|
|
static bool hub = false;
|
|
|
|
/* -n, --noflow: Pass traffic, but don't setup flows in switch */
|
|
static bool noflow = false;
|
|
|
|
static void parse_options(int argc, char *argv[]);
|
|
static void usage(void) NO_RETURN;
|
|
|
|
static struct switch_ *connect_switch(const char *name);
|
|
static struct switch_ *new_switch(const char *name, struct vconn *);
|
|
static void close_switch(struct switch_ *);
|
|
|
|
static void queue_tx(struct switch_ *, struct buffer *);
|
|
|
|
static void send_control_hello(struct switch_ *);
|
|
|
|
static int do_switch_recv(struct switch_ *this);
|
|
static int do_switch_send(struct switch_ *this);
|
|
|
|
static void process_packet(struct switch_ *, struct buffer *);
|
|
static void process_hub(struct switch_ *, struct ofp_packet_in *);
|
|
static void process_noflow(struct switch_ *, struct ofp_packet_in *);
|
|
|
|
static void switch_init(void);
|
|
static void process_switch(struct switch_ *, struct ofp_packet_in *);
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
struct switch_ *switches[MAX_SWITCHES];
|
|
int n_switches;
|
|
int retval;
|
|
int i;
|
|
|
|
set_program_name(argv[0]);
|
|
register_fault_handlers();
|
|
vlog_init();
|
|
parse_options(argc, argv);
|
|
|
|
if (!hub && !noflow) {
|
|
switch_init();
|
|
}
|
|
|
|
if (argc - optind < 1) {
|
|
fatal(0, "at least one vconn argument required; use --help for usage");
|
|
}
|
|
|
|
retval = vlog_server_listen(NULL, NULL);
|
|
if (retval) {
|
|
fatal(retval, "Could not listen for vlog connections");
|
|
}
|
|
|
|
n_switches = 0;
|
|
for (i = 0; i < argc - optind; i++) {
|
|
struct switch_ *this = connect_switch(argv[optind + i]);
|
|
if (this) {
|
|
if (n_switches >= MAX_SWITCHES) {
|
|
fatal(0, "max %d switch connections", n_switches);
|
|
}
|
|
switches[n_switches++] = this;
|
|
}
|
|
}
|
|
if (n_switches == 0) {
|
|
fatal(0, "could not connect to any switches");
|
|
}
|
|
|
|
while (n_switches > 0) {
|
|
/* Do some work. Limit the number of iterations so that callbacks
|
|
* registered with the poll loop don't starve. */
|
|
int iteration;
|
|
int i;
|
|
for (iteration = 0; iteration < 50; iteration++) {
|
|
bool progress = false;
|
|
for (i = 0; i < n_switches; ) {
|
|
struct switch_ *this = switches[i];
|
|
int retval;
|
|
|
|
if (vconn_is_passive(this->vconn)) {
|
|
retval = 0;
|
|
while (n_switches < MAX_SWITCHES) {
|
|
struct vconn *new_vconn;
|
|
retval = vconn_accept(this->vconn, &new_vconn);
|
|
if (retval) {
|
|
break;
|
|
}
|
|
switches[n_switches++] = new_switch("tcp", new_vconn);
|
|
}
|
|
} else {
|
|
retval = do_switch_recv(this);
|
|
if (!retval || retval == EAGAIN) {
|
|
do {
|
|
retval = do_switch_send(this);
|
|
if (!retval) {
|
|
progress = true;
|
|
}
|
|
} while (!retval);
|
|
}
|
|
}
|
|
|
|
if (retval && retval != EAGAIN) {
|
|
close_switch(this);
|
|
switches[i] = switches[--n_switches];
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
if (!progress) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Wait for something to happen. */
|
|
for (i = 0; i < n_switches; i++) {
|
|
struct switch_ *this = switches[i];
|
|
if (vconn_is_passive(this->vconn)) {
|
|
if (n_switches < MAX_SWITCHES) {
|
|
vconn_accept_wait(this->vconn);
|
|
}
|
|
} else {
|
|
vconn_recv_wait(this->vconn);
|
|
if (this->txq.n) {
|
|
vconn_send_wait(this->vconn);
|
|
}
|
|
}
|
|
}
|
|
poll_block();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
do_switch_recv(struct switch_ *this)
|
|
{
|
|
struct buffer *msg;
|
|
int retval;
|
|
|
|
retval = vconn_recv(this->vconn, &msg);
|
|
if (!retval) {
|
|
process_packet(this, msg);
|
|
buffer_delete(msg);
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
static int
|
|
do_switch_send(struct switch_ *this)
|
|
{
|
|
int retval = 0;
|
|
if (this->txq.n) {
|
|
struct buffer *next = this->txq.head->next;
|
|
retval = vconn_send(this->vconn, this->txq.head);
|
|
if (retval) {
|
|
return retval;
|
|
}
|
|
queue_advance_head(&this->txq, next);
|
|
return 0;
|
|
}
|
|
return EAGAIN;
|
|
}
|
|
|
|
struct switch_ *
|
|
connect_switch(const char *name)
|
|
{
|
|
struct vconn *vconn;
|
|
int retval;
|
|
|
|
retval = vconn_open(name, &vconn);
|
|
if (retval) {
|
|
VLOG_ERR("%s: connect: %s", name, strerror(retval));
|
|
return NULL;
|
|
}
|
|
|
|
return new_switch(name, vconn);
|
|
}
|
|
|
|
static struct switch_ *
|
|
new_switch(const char *name, struct vconn *vconn)
|
|
{
|
|
struct switch_ *this = xmalloc(sizeof *this);
|
|
memset(this, 0, sizeof *this);
|
|
this->name = xstrdup(name);
|
|
this->vconn = vconn;
|
|
queue_init(&this->txq);
|
|
this->last_control_hello = 0;
|
|
if (!vconn_is_passive(vconn)) {
|
|
send_control_hello(this);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
static void
|
|
close_switch(struct switch_ *this)
|
|
{
|
|
if (this) {
|
|
free(this->name);
|
|
vconn_close(this->vconn);
|
|
queue_destroy(&this->txq);
|
|
free(this);
|
|
}
|
|
}
|
|
|
|
static void
|
|
send_control_hello(struct switch_ *this)
|
|
{
|
|
time_t now = time(0);
|
|
if (now >= this->last_control_hello + 1) {
|
|
struct buffer *b;
|
|
struct ofp_control_hello *och;
|
|
|
|
b = buffer_new(0);
|
|
och = buffer_put_uninit(b, sizeof *och);
|
|
memset(och, 0, sizeof *och);
|
|
och->header.version = OFP_VERSION;
|
|
och->header.length = htons(sizeof *och);
|
|
|
|
och->version = htonl(OFP_VERSION);
|
|
och->flags = htons(OFP_CHELLO_SEND_FLOW_EXP);
|
|
och->miss_send_len = htons(OFP_DEFAULT_MISS_SEND_LEN);
|
|
queue_tx(this, b);
|
|
|
|
this->last_control_hello = now;
|
|
}
|
|
}
|
|
|
|
static void
|
|
queue_tx(struct switch_ *this, struct buffer *b)
|
|
{
|
|
queue_push_tail(&this->txq, b);
|
|
}
|
|
|
|
static void
|
|
process_packet(struct switch_ *sw, struct buffer *msg)
|
|
{
|
|
static const size_t min_size[UINT8_MAX + 1] = {
|
|
[0 ... UINT8_MAX] = SIZE_MAX,
|
|
[OFPT_CONTROL_HELLO] = sizeof (struct ofp_control_hello),
|
|
[OFPT_DATA_HELLO] = sizeof (struct ofp_data_hello),
|
|
[OFPT_PACKET_IN] = offsetof (struct ofp_packet_in, data),
|
|
[OFPT_PACKET_OUT] = sizeof (struct ofp_packet_out),
|
|
[OFPT_FLOW_MOD] = sizeof (struct ofp_flow_mod),
|
|
[OFPT_FLOW_EXPIRED] = sizeof (struct ofp_flow_expired),
|
|
[OFPT_TABLE] = sizeof (struct ofp_table),
|
|
[OFPT_PORT_MOD] = sizeof (struct ofp_port_mod),
|
|
[OFPT_PORT_STATUS] = sizeof (struct ofp_port_status),
|
|
[OFPT_FLOW_STAT_REQUEST] = sizeof (struct ofp_flow_stat_request),
|
|
[OFPT_FLOW_STAT_REPLY] = sizeof (struct ofp_flow_stat_reply),
|
|
};
|
|
struct ofp_header *oh;
|
|
|
|
oh = msg->data;
|
|
if (msg->size < min_size[oh->type]) {
|
|
VLOG_WARN("%s: too short (%zu bytes) for type %"PRIu8" (min %zu)",
|
|
sw->name, msg->size, oh->type, min_size[oh->type]);
|
|
return;
|
|
}
|
|
|
|
if (oh->type == OFPT_DATA_HELLO) {
|
|
struct ofp_data_hello *odh = msg->data;
|
|
sw->datapath_id = odh->datapath_id;
|
|
} else if (sw->datapath_id == 0) {
|
|
send_control_hello(sw);
|
|
return;
|
|
}
|
|
|
|
if (oh->type == OFPT_PACKET_IN) {
|
|
if (sw->txq.n >= MAX_TXQ) {
|
|
/* FIXME: ratelimit. */
|
|
VLOG_WARN("%s: tx queue overflow", sw->name);
|
|
} else if (noflow) {
|
|
process_noflow(sw, msg->data);
|
|
} else if (hub) {
|
|
process_hub(sw, msg->data);
|
|
} else {
|
|
process_switch(sw, msg->data);
|
|
}
|
|
return;
|
|
}
|
|
|
|
ofp_print(stdout, msg->data, msg->size, 2);
|
|
}
|
|
|
|
static void
|
|
process_hub(struct switch_ *sw, struct ofp_packet_in *opi)
|
|
{
|
|
size_t pkt_ofs, pkt_len;
|
|
struct buffer pkt;
|
|
struct flow flow;
|
|
|
|
/* Extract flow data from 'opi' into 'flow'. */
|
|
pkt_ofs = offsetof(struct ofp_packet_in, data);
|
|
pkt_len = ntohs(opi->header.length) - pkt_ofs;
|
|
pkt.data = opi->data;
|
|
pkt.size = pkt_len;
|
|
flow_extract(&pkt, ntohs(opi->in_port), &flow);
|
|
|
|
/* Add new flow. */
|
|
queue_tx(sw, make_add_simple_flow(&flow, ntohl(opi->buffer_id),
|
|
OFPP_FLOOD));
|
|
|
|
/* If the switch didn't buffer the packet, we need to send a copy. */
|
|
if (ntohl(opi->buffer_id) == UINT32_MAX) {
|
|
queue_tx(sw, make_unbuffered_packet_out(&pkt, ntohs(flow.in_port),
|
|
OFPP_FLOOD));
|
|
}
|
|
}
|
|
|
|
static void
|
|
process_noflow(struct switch_ *sw, struct ofp_packet_in *opi)
|
|
{
|
|
/* If the switch didn't buffer the packet, we need to send a copy. */
|
|
if (ntohl(opi->buffer_id) == UINT32_MAX) {
|
|
size_t pkt_ofs, pkt_len;
|
|
struct buffer pkt;
|
|
|
|
/* Extract flow data from 'opi' into 'flow'. */
|
|
pkt_ofs = offsetof(struct ofp_packet_in, data);
|
|
pkt_len = ntohs(opi->header.length) - pkt_ofs;
|
|
pkt.data = opi->data;
|
|
pkt.size = pkt_len;
|
|
|
|
queue_tx(sw, make_unbuffered_packet_out(&pkt, ntohs(opi->in_port),
|
|
OFPP_FLOOD));
|
|
} else {
|
|
queue_tx(sw, make_buffered_packet_out(ntohl(opi->buffer_id),
|
|
ntohs(opi->in_port), OFPP_FLOOD));
|
|
}
|
|
}
|
|
|
|
|
|
#define MAC_HASH_BITS 10
|
|
#define MAC_HASH_MASK (MAC_HASH_SIZE - 1)
|
|
#define MAC_HASH_SIZE (1u << MAC_HASH_BITS)
|
|
|
|
#define MAC_MAX 1024
|
|
|
|
struct mac_source {
|
|
struct list hash_list;
|
|
struct list lru_list;
|
|
uint64_t datapath_id;
|
|
uint8_t mac[ETH_ADDR_LEN];
|
|
uint16_t port;
|
|
};
|
|
|
|
static struct list mac_table[MAC_HASH_SIZE];
|
|
static struct list lrus;
|
|
static size_t mac_count;
|
|
|
|
static void
|
|
switch_init(void)
|
|
{
|
|
int i;
|
|
|
|
list_init(&lrus);
|
|
for (i = 0; i < MAC_HASH_SIZE; i++) {
|
|
list_init(&mac_table[i]);
|
|
}
|
|
}
|
|
|
|
static struct list *
|
|
mac_table_bucket(uint64_t datapath_id, const uint8_t mac[ETH_ADDR_LEN])
|
|
{
|
|
uint32_t hash;
|
|
hash = hash_fnv(&datapath_id, sizeof datapath_id, HASH_FNV_BASIS);
|
|
hash = hash_fnv(mac, ETH_ADDR_LEN, hash);
|
|
return &mac_table[hash & MAC_HASH_BITS];
|
|
}
|
|
|
|
static void
|
|
process_switch(struct switch_ *sw, struct ofp_packet_in *opi)
|
|
{
|
|
size_t pkt_ofs, pkt_len;
|
|
struct buffer pkt;
|
|
struct flow flow;
|
|
|
|
uint16_t out_port;
|
|
|
|
/* Extract flow data from 'opi' into 'flow'. */
|
|
pkt_ofs = offsetof(struct ofp_packet_in, data);
|
|
pkt_len = ntohs(opi->header.length) - pkt_ofs;
|
|
pkt.data = opi->data;
|
|
pkt.size = pkt_len;
|
|
flow_extract(&pkt, ntohs(opi->in_port), &flow);
|
|
|
|
/* Learn the source. */
|
|
if (!eth_addr_is_multicast(flow.dl_src)) {
|
|
struct mac_source *src;
|
|
struct list *bucket;
|
|
bool found;
|
|
|
|
bucket = mac_table_bucket(sw->datapath_id, flow.dl_src);
|
|
found = false;
|
|
LIST_FOR_EACH (src, struct mac_source, hash_list, bucket) {
|
|
if (src->datapath_id == sw->datapath_id
|
|
&& eth_addr_equals(src->mac, flow.dl_src)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
/* Learn a new address. */
|
|
|
|
if (mac_count >= MAC_MAX) {
|
|
/* Drop the least recently used mac source. */
|
|
struct mac_source *lru;
|
|
lru = CONTAINER_OF(lrus.next, struct mac_source, lru_list);
|
|
list_remove(&lru->hash_list);
|
|
list_remove(&lru->lru_list);
|
|
free(lru);
|
|
} else {
|
|
mac_count++;
|
|
}
|
|
|
|
/* Create new mac source */
|
|
src = xmalloc(sizeof *src);
|
|
src->datapath_id = sw->datapath_id;
|
|
memcpy(src->mac, flow.dl_src, ETH_ADDR_LEN);
|
|
src->port = -1;
|
|
list_push_front(bucket, &src->hash_list);
|
|
list_push_back(&lrus, &src->lru_list);
|
|
} else {
|
|
/* Make 'src' most-recently-used. */
|
|
list_remove(&src->lru_list);
|
|
list_push_back(&lrus, &src->lru_list);
|
|
}
|
|
|
|
if (ntohs(flow.in_port) != src->port) {
|
|
src->port = ntohs(flow.in_port);
|
|
VLOG_DBG("learned that "ETH_ADDR_FMT" is on datapath %"
|
|
PRIx64" port %d",
|
|
ETH_ADDR_ARGS(src->mac), ntohll(src->datapath_id),
|
|
src->port);
|
|
}
|
|
} else {
|
|
VLOG_DBG("multicast packet source "ETH_ADDR_FMT,
|
|
ETH_ADDR_ARGS(flow.dl_src));
|
|
}
|
|
|
|
/* Figure out the destination. */
|
|
out_port = OFPP_FLOOD;
|
|
if (!eth_addr_is_multicast(flow.dl_dst)) {
|
|
struct mac_source *dst;
|
|
struct list *bucket;
|
|
|
|
bucket = mac_table_bucket(sw->datapath_id, flow.dl_dst);
|
|
LIST_FOR_EACH (dst, struct mac_source, hash_list, bucket) {
|
|
if (dst->datapath_id == sw->datapath_id
|
|
&& eth_addr_equals(dst->mac, flow.dl_dst)) {
|
|
out_port = dst->port;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (out_port != OFPP_FLOOD) {
|
|
/* The output port is known, so add a new flow. */
|
|
queue_tx(sw, make_add_simple_flow(&flow, ntohl(opi->buffer_id),
|
|
out_port));
|
|
|
|
/* If the switch didn't buffer the packet, we need to send a copy. */
|
|
if (ntohl(opi->buffer_id) == UINT32_MAX) {
|
|
queue_tx(sw, make_unbuffered_packet_out(&pkt, ntohs(flow.in_port),
|
|
out_port));
|
|
}
|
|
} else {
|
|
/* We don't know that MAC. Flood the packet. */
|
|
struct buffer *b;
|
|
if (ntohl(opi->buffer_id) == UINT32_MAX) {
|
|
b = make_unbuffered_packet_out(&pkt, ntohs(flow.in_port), out_port);
|
|
} else {
|
|
b = make_buffered_packet_out(ntohl(opi->buffer_id),
|
|
ntohs(flow.in_port), out_port);
|
|
}
|
|
queue_tx(sw, b);
|
|
}
|
|
}
|
|
|
|
static void
|
|
parse_options(int argc, char *argv[])
|
|
{
|
|
static struct option long_options[] = {
|
|
{"hub", no_argument, 0, 'H'},
|
|
{"noflow", no_argument, 0, 'n'},
|
|
{"verbose", optional_argument, 0, 'v'},
|
|
{"help", no_argument, 0, 'h'},
|
|
{"version", no_argument, 0, 'V'},
|
|
#ifdef HAVE_OPENSSL
|
|
{"private-key", required_argument, 0, 'p'},
|
|
{"certificate", required_argument, 0, 'c'},
|
|
{"ca-cert", required_argument, 0, 'C'},
|
|
#endif
|
|
{0, 0, 0, 0},
|
|
};
|
|
char *short_options = long_options_to_short_options(long_options);
|
|
|
|
for (;;) {
|
|
int indexptr;
|
|
int c;
|
|
|
|
c = getopt_long(argc, argv, short_options, long_options, &indexptr);
|
|
if (c == -1) {
|
|
break;
|
|
}
|
|
|
|
switch (c) {
|
|
case 'H':
|
|
hub = true;
|
|
break;
|
|
|
|
case 'n':
|
|
noflow = true;
|
|
break;
|
|
|
|
case 'h':
|
|
usage();
|
|
|
|
case 'V':
|
|
printf("%s "VERSION" compiled "__DATE__" "__TIME__"\n", argv[0]);
|
|
exit(EXIT_SUCCESS);
|
|
|
|
case 'v':
|
|
vlog_set_verbosity(optarg);
|
|
break;
|
|
|
|
#ifdef HAVE_OPENSSL
|
|
case 'p':
|
|
vconn_ssl_set_private_key_file(optarg);
|
|
break;
|
|
|
|
case 'c':
|
|
vconn_ssl_set_certificate_file(optarg);
|
|
break;
|
|
|
|
case 'C':
|
|
vconn_ssl_set_ca_cert_file(optarg);
|
|
break;
|
|
#endif
|
|
|
|
case '?':
|
|
exit(EXIT_FAILURE);
|
|
|
|
default:
|
|
abort();
|
|
}
|
|
}
|
|
free(short_options);
|
|
}
|
|
|
|
static void
|
|
usage(void)
|
|
{
|
|
printf("%s: OpenFlow controller\n"
|
|
"usage: %s [OPTIONS] VCONN\n"
|
|
"where VCONN is one of the following:\n"
|
|
" ptcp:[PORT] listen to TCP PORT (default: %d)\n",
|
|
program_name, program_name, OFP_TCP_PORT);
|
|
#ifdef HAVE_NETLINK
|
|
printf(" nl:DP_IDX via netlink to local datapath DP_IDX\n");
|
|
#endif
|
|
#ifdef HAVE_OPENSSL
|
|
printf(" pssl:[PORT] listen for SSL on PORT (default: %d)\n"
|
|
"\nPKI configuration (required to use SSL):\n"
|
|
" -p, --private-key=FILE file with private key\n"
|
|
" -c, --certificate=FILE file with certificate for private key\n"
|
|
" -C, --ca-cert=FILE file with peer CA certificate\n",
|
|
OFP_SSL_PORT);
|
|
#endif
|
|
printf("\nOther options:\n"
|
|
" -H, --hub act as hub instead of learning switch\n"
|
|
" -n, --noflow pass traffic, but don't add flows\n"
|
|
" -v, --verbose set maximum verbosity level\n"
|
|
" -h, --help display this help message\n"
|
|
" -V, --version display version information\n");
|
|
exit(EXIT_SUCCESS);
|
|
}
|