1 /* $OpenBSD: connection.c,v 1.25 2025/01/28 20:41:44 claudio Exp $ */ 2 3 /* 4 * Copyright (c) 2009 Claudio Jeker <claudio@openbsd.org> 5 * 6 * Permission to use, copy, modify, and distribute this software for any 7 * purpose with or without fee is hereby granted, provided that the above 8 * copyright notice and this permission notice appear in all copies. 9 * 10 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 */ 18 19 #include <sys/types.h> 20 #include <sys/queue.h> 21 #include <sys/socket.h> 22 #include <sys/uio.h> 23 24 #include <netinet/in.h> 25 #include <netinet/tcp.h> 26 27 #include <scsi/iscsi.h> 28 29 #include <errno.h> 30 #include <event.h> 31 #include <stdio.h> 32 #include <stdlib.h> 33 #include <string.h> 34 #include <unistd.h> 35 36 #include "iscsid.h" 37 #include "log.h" 38 39 void conn_dispatch(int, short, void *); 40 void conn_write_dispatch(int, short, void *); 41 42 int c_do_connect(struct connection *, enum c_event); 43 int c_do_login(struct connection *, enum c_event); 44 int c_do_loggedin(struct connection *, enum c_event); 45 int c_do_req_logout(struct connection *, enum c_event); 46 int c_do_logout(struct connection *, enum c_event); 47 int c_do_loggedout(struct connection *, enum c_event); 48 int c_do_fail(struct connection *, enum c_event); 49 int c_do_cleanup(struct connection *, enum c_event); 50 51 const char *conn_state(int); 52 const char *conn_event(enum c_event); 53 54 void 55 conn_new(struct session *s, struct connection_config *cc) 56 { 57 struct connection *c; 58 int nodelay = 1; 59 60 if (!(c = calloc(1, sizeof(*c)))) 61 fatal("session_add_conn"); 62 63 c->fd = -1; 64 c->state = CONN_FREE; 65 c->session = s; 66 c->cid = arc4random(); 67 c->config = *cc; 68 c->mine = initiator_conn_defaults; 69 if (s->config.HeaderDigest != 0) 70 c->mine.HeaderDigest = s->config.HeaderDigest; 71 if (s->config.DataDigest != 0) 72 c->mine.DataDigest = s->config.DataDigest; 73 c->his = iscsi_conn_defaults; 74 75 c->sev.sess = s; 76 c->sev.conn = c; 77 evtimer_set(&c->sev.ev, session_fsm_callback, &c->sev); 78 79 TAILQ_INIT(&c->pdu_w); 80 TAILQ_INIT(&c->tasks); 81 TAILQ_INSERT_TAIL(&s->connections, c, entry); 82 83 if (pdu_readbuf_set(&c->prbuf, PDU_READ_SIZE)) { 84 log_warn("conn_new"); 85 conn_free(c); 86 return; 87 } 88 89 /* create socket */ 90 c->fd = socket(c->config.TargetAddr.ss_family, SOCK_STREAM, 0); 91 if (c->fd == -1) { 92 log_warn("conn_new: socket"); 93 conn_free(c); 94 return; 95 } 96 if (socket_setblockmode(c->fd, 1)) { 97 log_warn("conn_new: socket_setblockmode"); 98 conn_free(c); 99 return; 100 } 101 102 /* try to turn off TCP Nagle */ 103 if (setsockopt(c->fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, 104 sizeof(nodelay)) == -1) 105 log_warn("conn_new: setting TCP_NODELAY"); 106 107 event_set(&c->ev, c->fd, EV_READ|EV_PERSIST, conn_dispatch, c); 108 event_set(&c->wev, c->fd, EV_WRITE, conn_write_dispatch, c); 109 110 conn_fsm(c, CONN_EV_CONNECT); 111 } 112 113 void 114 conn_free(struct connection *c) 115 { 116 log_debug("conn_free"); 117 118 pdu_readbuf_free(&c->prbuf); 119 pdu_free_queue(&c->pdu_w); 120 121 event_del(&c->sev.ev); 122 event_del(&c->ev); 123 event_del(&c->wev); 124 if (c->fd != -1) 125 close(c->fd); 126 127 taskq_cleanup(&c->tasks); 128 129 TAILQ_REMOVE(&c->session->connections, c, entry); 130 free(c); 131 } 132 133 void 134 conn_dispatch(int fd, short event, void *arg) 135 { 136 struct connection *c = arg; 137 ssize_t n; 138 139 if (!(event & EV_READ)) { 140 log_debug("spurious read call"); 141 return; 142 } 143 if ((n = pdu_read(c)) == -1) { 144 if (errno == EAGAIN || errno == ENOBUFS || 145 errno == EINTR) /* try later */ 146 return; 147 log_warn("pdu_read"); 148 conn_fsm(c, CONN_EV_FAIL); 149 return; 150 } 151 if (n == 0) { /* connection closed */ 152 conn_fsm(c, CONN_EV_CLOSED); 153 return; 154 } 155 156 pdu_parse(c); 157 } 158 159 void 160 conn_write_dispatch(int fd, short event, void *arg) 161 { 162 struct connection *c = arg; 163 ssize_t n; 164 int error; 165 socklen_t len; 166 167 if (!(event & EV_WRITE)) { 168 log_debug("spurious write call"); 169 return; 170 } 171 172 switch (c->state) { 173 case CONN_XPT_WAIT: 174 len = sizeof(error); 175 if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR, 176 &error, &len) == -1 || (errno = error)) { 177 log_warn("connect to %s failed", 178 log_sockaddr(&c->config.TargetAddr)); 179 conn_fsm(c, CONN_EV_FAIL); 180 return; 181 } 182 conn_fsm(c, CONN_EV_CONNECTED); 183 break; 184 default: 185 if ((n = pdu_write(c)) == -1) { 186 log_warn("pdu_write"); 187 conn_fsm(c, CONN_EV_FAIL); 188 return; 189 } 190 if (n == 0) { /* connection closed */ 191 conn_fsm(c, CONN_EV_CLOSED); 192 return; 193 } 194 195 /* check if there is more to send */ 196 if (pdu_pending(c)) 197 event_add(&c->wev, NULL); 198 } 199 } 200 201 void 202 conn_fail(struct connection *c) 203 { 204 log_debug("conn_fail"); 205 conn_fsm(c, CONN_EV_FAIL); 206 } 207 208 int 209 conn_task_ready(struct connection *c) 210 { 211 if ((c->state & CONN_RUNNING) && TAILQ_EMPTY(&c->tasks)) 212 return 1; 213 return 0; 214 } 215 216 void 217 conn_task_issue(struct connection *c, struct task *t) 218 { 219 TAILQ_INSERT_TAIL(&c->tasks, t, entry); 220 conn_task_schedule(c); 221 } 222 223 void 224 conn_task_schedule(struct connection *c) 225 { 226 struct task *t = TAILQ_FIRST(&c->tasks); 227 struct pdu *p, *np; 228 229 if (!t) { 230 log_debug("conn_task_schedule: task is hiding"); 231 return; 232 } 233 234 /* move pdus to the write queue */ 235 for (p = TAILQ_FIRST(&t->sendq); p != NULL; p = np) { 236 np = TAILQ_NEXT(p, entry); 237 TAILQ_REMOVE(&t->sendq, p, entry); 238 conn_pdu_write(c, p); 239 } 240 if (t->callback == NULL) { 241 /* no callback, immediate command expecting no answer */ 242 conn_task_cleanup(c, t); 243 free(t); 244 } 245 } 246 247 void 248 conn_task_cleanup(struct connection *c, struct task *t) 249 { 250 pdu_free_queue(&t->sendq); 251 pdu_free_queue(&t->recvq); 252 /* XXX need some state to know if queued or not */ 253 if (c) { 254 TAILQ_REMOVE(&c->tasks, t, entry); 255 if (!TAILQ_EMPTY(&c->tasks)) 256 conn_task_schedule(c); 257 else 258 session_schedule(c->session); 259 } 260 } 261 262 #define SET_NUM(p, x, v, min, max) \ 263 do { \ 264 if (!strcmp((p)->key, #v)) { \ 265 (x)->his.v = text_to_num((p)->value, (min), (max), &err); \ 266 if (err) { \ 267 log_warnx("bad param %s=%s: %s", \ 268 (p)->key, (p)->value, err); \ 269 errors++; \ 270 } \ 271 } \ 272 } while (0) 273 274 #define SET_BOOL(p, x, v) \ 275 do { \ 276 if (!strcmp((p)->key, #v)) { \ 277 (x)->his.v = text_to_bool((p)->value, &err); \ 278 if (err) { \ 279 log_warnx("bad param %s=%s: %s", \ 280 (p)->key, (p)->value, err); \ 281 errors++; \ 282 } \ 283 } \ 284 } while (0) 285 286 #define SET_DIGEST(p, x, v) \ 287 do { \ 288 if (!strcmp((p)->key, #v)) { \ 289 (x)->his.v = text_to_digest((p)->value, &err); \ 290 if (err) { \ 291 log_warnx("bad param %s=%s: %s", \ 292 (p)->key, (p)->value, err); \ 293 errors++; \ 294 } \ 295 } \ 296 } while (0) 297 298 int 299 conn_parse_kvp(struct connection *c, struct kvp *kvp) 300 { 301 struct kvp *k; 302 struct session *s = c->session; 303 const char *err; 304 int errors = 0; 305 306 307 for (k = kvp; k->key; k++) { 308 log_debug("conn_parse_kvp: %s = %s", k->key, k->value); 309 /* XXX handle NotUnderstood|Irrelevant|Reject */ 310 SET_NUM(k, s, MaxBurstLength, 512, 16777215); 311 SET_NUM(k, s, FirstBurstLength, 512, 16777215); 312 SET_NUM(k, s, DefaultTime2Wait, 0, 3600); 313 SET_NUM(k, s, DefaultTime2Retain, 0, 3600); 314 SET_NUM(k, s, MaxOutstandingR2T, 1, 65535); 315 SET_NUM(k, s, TargetPortalGroupTag, 0, 65535); 316 SET_NUM(k, s, MaxConnections, 1, 65535); 317 SET_BOOL(k, s, InitialR2T); 318 SET_BOOL(k, s, ImmediateData); 319 SET_BOOL(k, s, DataPDUInOrder); 320 SET_BOOL(k, s, DataSequenceInOrder); 321 SET_NUM(k, s, ErrorRecoveryLevel, 0, 2); 322 SET_NUM(k, c, MaxRecvDataSegmentLength, 512, 16777215); 323 SET_DIGEST(k, c, HeaderDigest); 324 SET_DIGEST(k, c, DataDigest); 325 } 326 327 if (errors) { 328 log_warnx("conn_parse_kvp: errors found"); 329 return -1; 330 } 331 return 0; 332 } 333 334 #undef SET_NUM 335 #undef SET_BOOL 336 #undef SET_DIGEST 337 338 #define GET_BOOL_P(dst, req, k, src, f) \ 339 do { \ 340 if (f == 0 && !strcmp(req, #k)) { \ 341 (dst)->key = #k; \ 342 (dst)->value = ((src)->mine.k) ? "Yes" : "No"; \ 343 f++; \ 344 } \ 345 } while (0) 346 347 #define GET_DIGEST_P(dst, req, k, src, f) \ 348 do { \ 349 if (f == 0 && !strcmp(req, #k)) { \ 350 (dst)->key = #k; \ 351 (dst)->value = \ 352 ((src)->mine.k == DIGEST_NONE) ? "None" : "CRC32C,None";\ 353 f++; \ 354 } \ 355 } while (0) 356 357 #define GET_NUM_P(dst, req, k, src, f, e) \ 358 do { \ 359 if (f == 0 && !strcmp(req, #k)) { \ 360 (dst)->key = #k; \ 361 if (asprintf(&((dst)->value), "%u", (src)->mine.k) == -1)\ 362 e++; \ 363 else \ 364 (dst)->flags |= KVP_VALUE_ALLOCED; \ 365 f++; \ 366 } \ 367 } while (0) 368 369 #define GET_STR_C(dst, req, k, src, f) \ 370 do { \ 371 if (f == 0 && !strcmp(req, #k)) { \ 372 (dst)->key = #k; \ 373 (dst)->value = (src)->config.k; \ 374 f++; \ 375 } \ 376 } while (0) 377 378 #define GET_STYPE_C(dst, req, k, src, f) \ 379 do { \ 380 if (f == 0 && !strcmp(req, #k)) { \ 381 (dst)->key = #k; \ 382 (dst)->value = ((src)->config.k == SESSION_TYPE_DISCOVERY)\ 383 ? "Discovery" : "Normal"; \ 384 f++; \ 385 } \ 386 } while (0) 387 388 int 389 kvp_set_from_mine(struct kvp *kvp, const char *key, struct connection *c) 390 { 391 int e = 0, f = 0; 392 393 if (kvp->flags & KVP_KEY_ALLOCED) 394 free(kvp->key); 395 kvp->key = NULL; 396 if (kvp->flags & KVP_VALUE_ALLOCED) 397 free(kvp->value); 398 kvp->value = NULL; 399 kvp->flags = 0; 400 401 /* XXX handle at least CHAP */ 402 if (!strcmp(key, "AuthMethod")) { 403 kvp->key = "AuthMethod"; 404 kvp->value = "None"; 405 return 0; 406 } 407 GET_DIGEST_P(kvp, key, HeaderDigest, c, f); 408 GET_DIGEST_P(kvp, key, DataDigest, c, f); 409 GET_NUM_P(kvp, key, MaxConnections, c->session, f, e); 410 GET_STR_C(kvp, key, TargetName, c->session, f); 411 GET_STR_C(kvp, key, InitiatorName, c->session, f); 412 GET_BOOL_P(kvp, key, InitialR2T, c->session, f); 413 GET_BOOL_P(kvp, key, ImmediateData, c->session, f); 414 GET_NUM_P(kvp, key, MaxRecvDataSegmentLength, c, f, e); 415 GET_NUM_P(kvp, key, MaxBurstLength, c->session, f, e); 416 GET_NUM_P(kvp, key, FirstBurstLength, c->session, f, e); 417 GET_NUM_P(kvp, key, DefaultTime2Wait, c->session, f, e); 418 GET_NUM_P(kvp, key, DefaultTime2Retain, c->session, f, e); 419 GET_NUM_P(kvp, key, MaxOutstandingR2T, c->session, f, e); 420 GET_BOOL_P(kvp, key, DataPDUInOrder, c->session, f); 421 GET_BOOL_P(kvp, key, DataSequenceInOrder, c->session, f); 422 GET_NUM_P(kvp, key, ErrorRecoveryLevel, c->session, f, e); 423 GET_STYPE_C(kvp, key, SessionType, c->session, f); 424 /* XXX handle TaskReporting */ 425 426 if (f == 0) { 427 errno = EINVAL; 428 return 1; 429 } 430 431 return e; 432 } 433 434 #undef GET_BOOL_P 435 #undef GET_DIGEST_P 436 #undef GET_NUM_P 437 #undef GET_STR_C 438 #undef GET_STYPE_C 439 440 void 441 conn_pdu_write(struct connection *c, struct pdu *p) 442 { 443 struct iscsi_pdu *ipdu; 444 445 /* XXX I GUESS THIS SHOULD BE MOVED TO PDU SOMEHOW... */ 446 ipdu = pdu_getbuf(p, NULL, PDU_HEADER); 447 switch (ISCSI_PDU_OPCODE(ipdu->opcode)) { 448 case ISCSI_OP_I_NOP: 449 case ISCSI_OP_SCSI_REQUEST: 450 case ISCSI_OP_TASK_REQUEST: 451 case ISCSI_OP_LOGIN_REQUEST: 452 case ISCSI_OP_TEXT_REQUEST: 453 case ISCSI_OP_DATA_OUT: 454 case ISCSI_OP_LOGOUT_REQUEST: 455 case ISCSI_OP_SNACK_REQUEST: 456 ipdu->expstatsn = ntohl(c->expstatsn); 457 break; 458 } 459 460 TAILQ_INSERT_TAIL(&c->pdu_w, p, entry); 461 event_add(&c->wev, NULL); 462 } 463 464 /* connection state machine more or less as specified in the RFC */ 465 struct { 466 int state; 467 enum c_event event; 468 int (*action)(struct connection *, enum c_event); 469 } fsm[] = { 470 { CONN_FREE, CONN_EV_CONNECT, c_do_connect }, /* T1 */ 471 { CONN_XPT_WAIT, CONN_EV_CONNECTED, c_do_login }, /* T4 */ 472 { CONN_IN_LOGIN, CONN_EV_LOGGED_IN, c_do_loggedin }, /* T5 */ 473 { CONN_LOGGED_IN, CONN_EV_LOGOUT, c_do_logout }, /* T9 */ 474 { CONN_LOGGED_IN, CONN_EV_REQ_LOGOUT, c_do_req_logout },/* T11 */ 475 { CONN_LOGOUT_REQ, CONN_EV_LOGOUT, c_do_logout }, /* T10 */ 476 { CONN_LOGOUT_REQ, CONN_EV_REQ_LOGOUT, c_do_req_logout},/* T12 */ 477 { CONN_LOGOUT_REQ, CONN_EV_LOGGED_OUT, c_do_loggedout },/* T18 */ 478 { CONN_IN_LOGOUT, CONN_EV_LOGGED_OUT, c_do_loggedout }, /* T13 */ 479 { CONN_IN_LOGOUT, CONN_EV_REQ_LOGOUT, c_do_req_logout },/* T14 */ 480 { CONN_CLEANUP_WAIT, CONN_EV_CLEANING_UP, c_do_cleanup},/* M2 */ 481 { CONN_CLEANUP_WAIT, CONN_EV_FREE, c_do_loggedout }, /* M1 */ 482 { CONN_IN_CLEANUP, CONN_EV_FREE, c_do_loggedout }, /* M4 */ 483 { CONN_IN_CLEANUP, CONN_EV_CLEANING_UP, c_do_cleanup}, 484 /* either one of T2, T7, T15, T16, T17, M3 */ 485 { CONN_ANYSTATE, CONN_EV_CLOSED, c_do_fail }, 486 { CONN_ANYSTATE, CONN_EV_FAIL, c_do_fail }, 487 { CONN_ANYSTATE, CONN_EV_FREE, c_do_fail }, 488 { 0, 0, NULL } 489 }; 490 491 void 492 conn_fsm(struct connection *c, enum c_event event) 493 { 494 int i, ns; 495 496 for (i = 0; fsm[i].action != NULL; i++) { 497 if (c->state & fsm[i].state && event == fsm[i].event) { 498 log_debug("conn_fsm[%s]: %s ev %s", 499 c->session->config.SessionName, 500 conn_state(c->state), conn_event(event)); 501 ns = fsm[i].action(c, event); 502 if (ns == -1) 503 /* XXX better please */ 504 fatalx("conn_fsm: action failed"); 505 log_debug("conn_fsm[%s]: new state %s", 506 c->session->config.SessionName, conn_state(ns)); 507 c->state = ns; 508 return; 509 } 510 } 511 log_warnx("conn_fsm[%s]: unhandled state transition [%s, %s]", 512 c->session->config.SessionName, conn_state(c->state), 513 conn_event(event)); 514 fatalx("bork bork bork"); 515 } 516 517 int 518 c_do_connect(struct connection *c, enum c_event ev) 519 { 520 if (c->fd == -1) { 521 log_warnx("connect(%s), lost socket", 522 log_sockaddr(&c->config.TargetAddr)); 523 session_fsm(&c->sev, SESS_EV_CONN_FAIL, 0); 524 return CONN_FREE; 525 } 526 if (c->config.LocalAddr.ss_len != 0) { 527 if (bind(c->fd, (struct sockaddr *)&c->config.LocalAddr, 528 c->config.LocalAddr.ss_len) == -1) { 529 log_warn("bind(%s)", 530 log_sockaddr(&c->config.LocalAddr)); 531 session_fsm(&c->sev, SESS_EV_CONN_FAIL, 0); 532 return CONN_FREE; 533 } 534 } 535 if (connect(c->fd, (struct sockaddr *)&c->config.TargetAddr, 536 c->config.TargetAddr.ss_len) == -1) { 537 if (errno == EINPROGRESS) { 538 event_add(&c->wev, NULL); 539 event_add(&c->ev, NULL); 540 return CONN_XPT_WAIT; 541 } else { 542 log_warn("connect(%s)", 543 log_sockaddr(&c->config.TargetAddr)); 544 session_fsm(&c->sev, SESS_EV_CONN_FAIL, 0); 545 return CONN_FREE; 546 } 547 } 548 event_add(&c->ev, NULL); 549 /* move forward */ 550 return c_do_login(c, CONN_EV_CONNECTED); 551 } 552 553 int 554 c_do_login(struct connection *c, enum c_event ev) 555 { 556 /* start a login session and hope for the best ... */ 557 initiator_login(c); 558 return CONN_IN_LOGIN; 559 } 560 561 int 562 c_do_loggedin(struct connection *c, enum c_event ev) 563 { 564 iscsi_merge_conn_params(&c->active, &c->mine, &c->his); 565 session_fsm(&c->sev, SESS_EV_CONN_LOGGED_IN, 0); 566 567 return CONN_LOGGED_IN; 568 } 569 570 int 571 c_do_req_logout(struct connection *c, enum c_event ev) 572 { 573 /* target requested logout. XXX implement async handler */ 574 575 if (c->state & CONN_IN_LOGOUT) 576 return CONN_IN_LOGOUT; 577 else 578 return CONN_LOGOUT_REQ; 579 } 580 581 int 582 c_do_logout(struct connection *c, enum c_event ev) 583 { 584 /* logout is in progress ... */ 585 return CONN_IN_LOGOUT; 586 } 587 588 int 589 c_do_loggedout(struct connection *c, enum c_event ev) 590 { 591 /* 592 * Called by the session fsm before calling conn_free. 593 * Doing this so the state transition is logged. 594 */ 595 return CONN_FREE; 596 } 597 598 int 599 c_do_fail(struct connection *c, enum c_event ev) 600 { 601 log_debug("c_do_fail"); 602 603 /* cleanup events so that the connection does not retrigger */ 604 event_del(&c->ev); 605 event_del(&c->wev); 606 close(c->fd); 607 c->fd = -1; /* make sure this fd is not closed again */ 608 609 /* all pending task have failed so clean them up */ 610 taskq_cleanup(&c->tasks); 611 612 /* session will take care of cleaning up the mess */ 613 session_fsm(&c->sev, SESS_EV_CONN_FAIL, 0); 614 615 if (ev == CONN_EV_FREE || c->state & CONN_NEVER_LOGGED_IN) 616 return CONN_FREE; 617 return CONN_CLEANUP_WAIT; 618 } 619 620 int 621 c_do_cleanup(struct connection *c, enum c_event ev) 622 { 623 /* nothing to do here just adjust state */ 624 return CONN_IN_CLEANUP; 625 } 626 627 const char * 628 conn_state(int s) 629 { 630 static char buf[15]; 631 632 switch (s) { 633 case CONN_FREE: 634 return "FREE"; 635 case CONN_XPT_WAIT: 636 return "XPT_WAIT"; 637 case CONN_XPT_UP: 638 return "XPT_UP"; 639 case CONN_IN_LOGIN: 640 return "IN_LOGIN"; 641 case CONN_LOGGED_IN: 642 return "LOGGED_IN"; 643 case CONN_IN_LOGOUT: 644 return "IN_LOGOUT"; 645 case CONN_LOGOUT_REQ: 646 return "LOGOUT_REQ"; 647 case CONN_CLEANUP_WAIT: 648 return "CLEANUP_WAIT"; 649 case CONN_IN_CLEANUP: 650 return "IN_CLEANUP"; 651 default: 652 snprintf(buf, sizeof(buf), "UKNWN %x", s); 653 return buf; 654 } 655 /* NOTREACHED */ 656 } 657 658 const char * 659 conn_event(enum c_event e) 660 { 661 static char buf[15]; 662 663 switch (e) { 664 case CONN_EV_FAIL: 665 return "fail"; 666 case CONN_EV_CONNECT: 667 return "connect"; 668 case CONN_EV_CONNECTED: 669 return "connected"; 670 case CONN_EV_LOGGED_IN: 671 return "logged in"; 672 case CONN_EV_REQ_LOGOUT: 673 return "logout requested"; 674 case CONN_EV_LOGOUT: 675 return "logout"; 676 case CONN_EV_LOGGED_OUT: 677 return "logged out"; 678 case CONN_EV_CLEANING_UP: 679 return "cleaning up"; 680 case CONN_EV_CLOSED: 681 return "closed"; 682 case CONN_EV_FREE: 683 return "forced free"; 684 } 685 686 snprintf(buf, sizeof(buf), "UKNWN %d", e); 687 return buf; 688 } 689