Changes in freeDiameter/sctp.c [29:5ba91682f0bc:20:277ec00d793e] in freeDiameter
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
freeDiameter/sctp.c
r29 r20 35 35 36 36 #include "fD.h" 37 #include "cnxctx.h"38 37 39 #include <netinet/sctp.h> 40 #include <sys/uio.h> 41 42 /* Size of buffer to receive ancilliary data. May need to be enlarged if more sockopt are set... */ 43 #ifndef CMSG_BUF_LEN 44 #define CMSG_BUF_LEN 1024 45 #endif /* CMSG_BUF_LEN */ 46 47 /* Level of SCTP-specific traces */ 48 #ifdef DEBUG_SCTP 49 #define SCTP_LEVEL FULL 50 #else /* DEBUG_SCTP */ 51 #define SCTP_LEVEL ANNOYING 52 #endif /* DEBUG_SCTP */ 53 54 /* Pre-binding socket options -- # streams read in config */ 55 static int fd_setsockopt_prebind(int sk) 38 int fd_sctp_create_bind_server( int * socket, uint16_t port ) 56 39 { 57 socklen_t sz;40 TODO("Create sctp server, using fd_g_config: cnf_endpoints, no_ip4, no_ip6, cnf_sctp_str"); 58 41 59 TRACE_ENTRY( "%d", sk); 60 61 CHECK_PARAMS( sk > 0 ); 62 63 /* Subscribe to some notifications */ 64 { 65 struct sctp_event_subscribe event; 66 67 memset(&event, 0, sizeof(event)); 68 event.sctp_data_io_event = 1; /* to receive the stream ID in SCTP_SNDRCV ancilliary data on message reception */ 69 event.sctp_association_event = 0; /* new or closed associations (mostly for one-to-many style sockets) */ 70 event.sctp_address_event = 1; /* address changes */ 71 event.sctp_send_failure_event = 1; /* delivery failures */ 72 event.sctp_peer_error_event = 1; /* remote peer sends an error */ 73 event.sctp_shutdown_event = 1; /* peer has sent a SHUTDOWN */ 74 event.sctp_partial_delivery_event = 1; /* a partial delivery is aborted, probably indicating the connection is being shutdown */ 75 // event.sctp_adaptation_layer_event = 0; /* adaptation layer notifications */ 76 // event.sctp_authentication_event = 0; /* when new key is made active */ 77 78 /* Set the option to the socket */ 79 CHECK_SYS( setsockopt(sk, IPPROTO_SCTP, SCTP_EVENTS, &event, sizeof(event)) ); 80 81 if (TRACE_BOOL(SCTP_LEVEL)) { 82 sz = sizeof(event); 83 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_EVENTS, &event, &sz) ); 84 if (sz != sizeof(event)) 85 { 86 TRACE_DEBUG(INFO, "Invalid size of socket option: %d / %d", sz, (socklen_t)sizeof(event)); 87 return ENOTSUP; 88 } 89 90 fd_log_debug( "SCTP_EVENTS : sctp_data_io_event : %hhu\n", event.sctp_data_io_event); 91 fd_log_debug( " sctp_association_event : %hhu\n", event.sctp_association_event); 92 fd_log_debug( " sctp_address_event : %hhu\n", event.sctp_address_event); 93 fd_log_debug( " sctp_send_failure_event : %hhu\n", event.sctp_send_failure_event); 94 fd_log_debug( " sctp_peer_error_event : %hhu\n", event.sctp_peer_error_event); 95 fd_log_debug( " sctp_shutdown_event : %hhu\n", event.sctp_shutdown_event); 96 fd_log_debug( " sctp_partial_delivery_event : %hhu\n", event.sctp_partial_delivery_event); 97 fd_log_debug( " sctp_adaptation_layer_event : %hhu\n", event.sctp_adaptation_layer_event); 98 // fd_log_debug( " sctp_authentication_event : %hhu\n", event.sctp_authentication_event); 99 } 100 101 } 102 103 /* Set the INIT parameters, such as number of streams */ 104 { 105 struct sctp_initmsg init; 106 memset(&init, 0, sizeof(init)); 107 108 if (TRACE_BOOL(SCTP_LEVEL)) { 109 sz = sizeof(init); 110 111 /* Read socket defaults */ 112 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_INITMSG, &init, &sz) ); 113 if (sz != sizeof(init)) 114 { 115 TRACE_DEBUG(INFO, "Invalid size of socket option: %d / %d", sz, (socklen_t)sizeof(init)); 116 return ENOTSUP; 117 } 118 fd_log_debug( "Def SCTP_INITMSG : sinit_num_ostreams : %hu\n", init.sinit_num_ostreams); 119 fd_log_debug( " sinit_max_instreams : %hu\n", init.sinit_max_instreams); 120 fd_log_debug( " sinit_max_attempts : %hu\n", init.sinit_max_attempts); 121 fd_log_debug( " sinit_max_init_timeo : %hu\n", init.sinit_max_init_timeo); 122 } 123 124 /* Set the init options -- need to receive SCTP_COMM_UP to confirm the requested parameters */ 125 init.sinit_num_ostreams = fd_g_config->cnf_sctp_str; /* desired number of outgoing streams */ 126 init.sinit_max_init_timeo = CNX_TIMEOUT * 1000; 127 128 /* Set the option to the socket */ 129 CHECK_SYS( setsockopt(sk, IPPROTO_SCTP, SCTP_INITMSG, &init, sizeof(init)) ); 130 131 if (TRACE_BOOL(SCTP_LEVEL)) { 132 /* Check new values */ 133 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_INITMSG, &init, &sz) ); 134 fd_log_debug( "New SCTP_INITMSG : sinit_num_ostreams : %hu\n", init.sinit_num_ostreams); 135 fd_log_debug( " sinit_max_instreams : %hu\n", init.sinit_max_instreams); 136 fd_log_debug( " sinit_max_attempts : %hu\n", init.sinit_max_attempts); 137 fd_log_debug( " sinit_max_init_timeo : %hu\n", init.sinit_max_init_timeo); 138 } 139 } 140 141 /* Set the SCTP_DISABLE_FRAGMENTS option, required for TLS */ 142 #ifdef SCTP_DISABLE_FRAGMENTS 143 { 144 int nofrag; 145 146 if (TRACE_BOOL(SCTP_LEVEL)) { 147 sz = sizeof(nofrag); 148 /* Read socket defaults */ 149 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_DISABLE_FRAGMENTS, &nofrag, &sz) ); 150 if (sz != sizeof(nofrag)) 151 { 152 TRACE_DEBUG(INFO, "Invalid size of socket option: %d / %d", sz, (socklen_t)sizeof(nofrag)); 153 return ENOTSUP; 154 } 155 fd_log_debug( "Def SCTP_DISABLE_FRAGMENTS value : %s\n", nofrag ? "true" : "false"); 156 } 157 158 nofrag = 0; /* We turn ON the fragmentation */ 159 160 /* Set the option to the socket */ 161 CHECK_SYS( setsockopt(sk, IPPROTO_SCTP, SCTP_DISABLE_FRAGMENTS, &nofrag, sizeof(nofrag)) ); 162 163 if (TRACE_BOOL(SCTP_LEVEL)) { 164 /* Check new values */ 165 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_DISABLE_FRAGMENTS, &nofrag, &sz) ); 166 fd_log_debug( "New SCTP_DISABLE_FRAGMENTS value : %s\n", nofrag ? "true" : "false"); 167 } 168 } 169 #else /* SCTP_DISABLE_FRAGMENTS */ 170 # error "TLS requires support of SCTP_DISABLE_FRAGMENTS" 171 #endif /* SCTP_DISABLE_FRAGMENTS */ 172 173 174 /* Set the RETRANSMIT parameters */ 175 #ifdef SCTP_RTOINFO 176 { 177 struct sctp_rtoinfo rtoinfo; 178 memset(&rtoinfo, 0, sizeof(rtoinfo)); 179 180 if (TRACE_BOOL(SCTP_LEVEL)) { 181 sz = sizeof(rtoinfo); 182 /* Read socket defaults */ 183 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_RTOINFO, &rtoinfo, &sz) ); 184 if (sz != sizeof(rtoinfo)) 185 { 186 TRACE_DEBUG(INFO, "Invalid size of socket option: %d / %d", sz, (socklen_t)sizeof(rtoinfo)); 187 return ENOTSUP; 188 } 189 fd_log_debug( "Def SCTP_RTOINFO : srto_initial : %u\n", rtoinfo.srto_initial); 190 fd_log_debug( " srto_max : %u\n", rtoinfo.srto_max); 191 fd_log_debug( " srto_min : %u\n", rtoinfo.srto_min); 192 } 193 194 rtoinfo.srto_max = fd_g_config->cnf_timer_tw * 500 - 1000; /* Maximum retransmit timer (in ms) (set to Tw / 2 - 1) */ 195 196 /* Set the option to the socket */ 197 CHECK_SYS( setsockopt(sk, IPPROTO_SCTP, SCTP_RTOINFO, &rtoinfo, sizeof(rtoinfo)) ); 198 199 if (TRACE_BOOL(SCTP_LEVEL)) { 200 /* Check new values */ 201 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_RTOINFO, &rtoinfo, &sz) ); 202 fd_log_debug( "New SCTP_RTOINFO : srto_initial : %u\n", rtoinfo.srto_initial); 203 fd_log_debug( " srto_max : %u\n", rtoinfo.srto_max); 204 fd_log_debug( " srto_min : %u\n", rtoinfo.srto_min); 205 } 206 } 207 #else /* SCTP_RTOINFO */ 208 TRACE_DEBUG(SCTP_LEVEL, "Skipping SCTP_RTOINFO"); 209 #endif /* SCTP_RTOINFO */ 210 211 /* Set the ASSOCIATION parameters */ 212 #ifdef SCTP_ASSOCINFO 213 { 214 struct sctp_assocparams assoc; 215 memset(&assoc, 0, sizeof(assoc)); 216 217 if (TRACE_BOOL(SCTP_LEVEL)) { 218 sz = sizeof(assoc); 219 /* Read socket defaults */ 220 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_ASSOCINFO, &assoc, &sz) ); 221 if (sz != sizeof(assoc)) 222 { 223 TRACE_DEBUG(INFO, "Invalid size of socket option: %d / %d", sz, (socklen_t)sizeof(assoc)); 224 return ENOTSUP; 225 } 226 fd_log_debug( "Def SCTP_ASSOCINFO : sasoc_asocmaxrxt : %hu\n", assoc.sasoc_asocmaxrxt); 227 fd_log_debug( " sasoc_number_peer_destinations : %hu\n", assoc.sasoc_number_peer_destinations); 228 fd_log_debug( " sasoc_peer_rwnd : %u\n" , assoc.sasoc_peer_rwnd); 229 fd_log_debug( " sasoc_local_rwnd : %u\n" , assoc.sasoc_local_rwnd); 230 fd_log_debug( " sasoc_cookie_life : %u\n" , assoc.sasoc_cookie_life); 231 } 232 233 assoc.sasoc_asocmaxrxt = 5; /* Maximum retransmission attempts: we want fast detection of errors */ 234 235 /* Set the option to the socket */ 236 CHECK_SYS( setsockopt(sk, IPPROTO_SCTP, SCTP_ASSOCINFO, &assoc, sizeof(assoc)) ); 237 238 if (TRACE_BOOL(SCTP_LEVEL)) { 239 /* Check new values */ 240 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_ASSOCINFO, &assoc, &sz) ); 241 fd_log_debug( "New SCTP_ASSOCINFO : sasoc_asocmaxrxt : %hu\n", assoc.sasoc_asocmaxrxt); 242 fd_log_debug( " sasoc_number_peer_destinations : %hu\n", assoc.sasoc_number_peer_destinations); 243 fd_log_debug( " sasoc_peer_rwnd : %u\n" , assoc.sasoc_peer_rwnd); 244 fd_log_debug( " sasoc_local_rwnd : %u\n" , assoc.sasoc_local_rwnd); 245 fd_log_debug( " sasoc_cookie_life : %u\n" , assoc.sasoc_cookie_life); 246 } 247 } 248 #else /* SCTP_ASSOCINFO */ 249 TRACE_DEBUG(SCTP_LEVEL, "Skipping SCTP_ASSOCINFO"); 250 #endif /* SCTP_ASSOCINFO */ 251 252 253 /* The SO_LINGER option will be re-set if we want to perform SCTP ABORT */ 254 #ifdef SO_LINGER 255 { 256 struct linger linger; 257 memset(&linger, 0, sizeof(linger)); 258 259 if (TRACE_BOOL(SCTP_LEVEL)) { 260 sz = sizeof(linger); 261 /* Read socket defaults */ 262 CHECK_SYS( getsockopt(sk, SOL_SOCKET, SO_LINGER, &linger, &sz) ); 263 if (sz != sizeof(linger)) 264 { 265 TRACE_DEBUG(INFO, "Invalid size of socket option: %d / %d", sz, (socklen_t)sizeof(linger)); 266 return ENOTSUP; 267 } 268 fd_log_debug( "Def SO_LINGER : l_onoff : %d\n", linger.l_onoff); 269 fd_log_debug( " l_linger : %d\n", linger.l_linger); 270 } 271 272 linger.l_onoff = 0; /* Do not activate the linger */ 273 linger.l_linger = 0; /* Return immediately when closing (=> abort) */ 274 275 /* Set the option */ 276 CHECK_SYS( setsockopt(sk, SOL_SOCKET, SO_LINGER, &linger, sizeof(linger)) ); 277 278 if (TRACE_BOOL(SCTP_LEVEL)) { 279 /* Check new values */ 280 CHECK_SYS( getsockopt(sk, SOL_SOCKET, SO_LINGER, &linger, &sz) ); 281 fd_log_debug( "New SO_LINGER : l_onoff : %d\n", linger.l_onoff); 282 fd_log_debug( " l_linger : %d\n", linger.l_linger); 283 } 284 } 285 #else /* SO_LINGER */ 286 TRACE_DEBUG(SCTP_LEVEL, "Skipping SO_LINGER"); 287 #endif /* SO_LINGER */ 288 289 /* Set the NODELAY option (Nagle-like algorithm) */ 290 #ifdef SCTP_NODELAY 291 { 292 int nodelay; 293 294 if (TRACE_BOOL(SCTP_LEVEL)) { 295 sz = sizeof(nodelay); 296 /* Read socket defaults */ 297 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_NODELAY, &nodelay, &sz) ); 298 if (sz != sizeof(nodelay)) 299 { 300 TRACE_DEBUG(INFO, "Invalid size of socket option: %d / %d", sz, (socklen_t)sizeof(nodelay)); 301 return ENOTSUP; 302 } 303 fd_log_debug( "Def SCTP_NODELAY value : %s\n", nodelay ? "true" : "false"); 304 } 305 306 nodelay = 0; /* We turn ON the Nagle algorithm (probably the default already) */ 307 308 /* Set the option to the socket */ 309 CHECK_SYS( setsockopt(sk, IPPROTO_SCTP, SCTP_NODELAY, &nodelay, sizeof(nodelay)) ); 310 311 if (TRACE_BOOL(SCTP_LEVEL)) { 312 /* Check new values */ 313 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_NODELAY, &nodelay, &sz) ); 314 fd_log_debug( "New SCTP_NODELAY value : %s\n", nodelay ? "true" : "false"); 315 } 316 } 317 #else /* SCTP_NODELAY */ 318 TRACE_DEBUG(SCTP_LEVEL, "Skipping SCTP_NODELAY"); 319 #endif /* SCTP_NODELAY */ 320 321 /* Set the interleaving option */ 322 #ifdef SCTP_FRAGMENT_INTERLEAVE 323 { 324 int interleave; 325 326 if (TRACE_BOOL(SCTP_LEVEL)) { 327 sz = sizeof(interleave); 328 /* Read socket defaults */ 329 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_FRAGMENT_INTERLEAVE, &interleave, &sz) ); 330 if (sz != sizeof(interleave)) 331 { 332 TRACE_DEBUG(INFO, "Invalid size of socket option: %d / %d", sz, (socklen_t)sizeof(interleave)); 333 return ENOTSUP; 334 } 335 fd_log_debug( "Def SCTP_FRAGMENT_INTERLEAVE value : %d\n", interleave); 336 } 337 338 #if 0 339 interleave = 2; /* Allow partial delivery on several streams at the same time, since we are stream-aware in our security modules */ 340 #else /* 0 */ 341 interleave = 1; /* hmmm actually, we are not yet capable of handling this, and we don t need it. */ 342 #endif /* 0 */ 343 344 /* Set the option to the socket */ 345 CHECK_SYS( setsockopt(sk, IPPROTO_SCTP, SCTP_FRAGMENT_INTERLEAVE, &interleave, sizeof(interleave)) ); 346 347 if (TRACE_BOOL(SCTP_LEVEL)) { 348 /* Check new values */ 349 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_FRAGMENT_INTERLEAVE, &interleave, &sz) ); 350 fd_log_debug( "New SCTP_FRAGMENT_INTERLEAVE value : %d\n", interleave); 351 } 352 } 353 #else /* SCTP_FRAGMENT_INTERLEAVE */ 354 TRACE_DEBUG(SCTP_LEVEL, "Skipping SCTP_FRAGMENT_INTERLEAVE"); 355 #endif /* SCTP_FRAGMENT_INTERLEAVE */ 356 357 /* Set the v4 mapped addresses option */ 358 #ifdef SCTP_I_WANT_MAPPED_V4_ADDR 359 { 360 int v4mapped; 361 362 if (TRACE_BOOL(SCTP_LEVEL)) { 363 sz = sizeof(v4mapped); 364 /* Read socket defaults */ 365 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_I_WANT_MAPPED_V4_ADDR, &v4mapped, &sz) ); 366 if (sz != sizeof(v4mapped)) 367 { 368 TRACE_DEBUG(INFO, "Invalid size of socket option: %d / %d", sz, (socklen_t)sizeof(v4mapped)); 369 return ENOTSUP; 370 } 371 fd_log_debug( "Def SCTP_I_WANT_MAPPED_V4_ADDR value : %s\n", v4mapped ? "true" : "false"); 372 } 373 374 #ifndef SCTP_USE_MAPPED_ADDRESSES 375 v4mapped = 0; /* We don't want v4 mapped addresses */ 376 #else /* SCTP_USE_MAPPED_ADDRESSES */ 377 v4mapped = 1; /* but we may have to, otherwise the bind fails in some environments */ 378 #endif /* SCTP_USE_MAPPED_ADDRESSES */ 379 380 /* Set the option to the socket */ 381 CHECK_SYS( setsockopt(sk, IPPROTO_SCTP, SCTP_I_WANT_MAPPED_V4_ADDR, &v4mapped, sizeof(v4mapped)) ); 382 383 if (TRACE_BOOL(SCTP_LEVEL)) { 384 /* Check new values */ 385 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_I_WANT_MAPPED_V4_ADDR, &v4mapped, &sz) ); 386 fd_log_debug( "New SCTP_I_WANT_MAPPED_V4_ADDR value : %s\n", v4mapped ? "true" : "false"); 387 } 388 } 389 #else /* SCTP_I_WANT_MAPPED_V4_ADDR */ 390 TRACE_DEBUG(SCTP_LEVEL, "Skipping SCTP_I_WANT_MAPPED_V4_ADDR"); 391 #endif /* SCTP_I_WANT_MAPPED_V4_ADDR */ 392 393 394 /* Other settable options (draft-ietf-tsvwg-sctpsocket-17): 395 SO_RCVBUF size of receiver window 396 SO_SNDBUF size of pending data to send 397 SCTP_AUTOCLOSE for one-to-many only 398 SCTP_SET_PEER_PRIMARY_ADDR ask remote peer to use this local address as primary 399 SCTP_PRIMARY_ADDR use this address as primary locally 400 SCTP_ADAPTATION_LAYER set adaptation layer indication 401 SCTP_PEER_ADDR_PARAMS control heartbeat per peer address 402 SCTP_DEFAULT_SEND_PARAM parameters for the sendto() call 403 SCTP_MAXSEG max size of fragmented segments -- bound to PMTU 404 SCTP_AUTH_CHUNK request authentication of some type of chunk 405 SCTP_HMAC_IDENT authentication algorithms 406 SCTP_AUTH_KEY set a shared key 407 SCTP_AUTH_ACTIVE_KEY set the active key 408 SCTP_AUTH_DELETE_KEY remove a key 409 SCTP_AUTH_DEACTIVATE_KEY will not use that key anymore 410 SCTP_DELAYED_SACK control delayed acks 411 SCTP_PARTIAL_DELIVERY_POINT control partial delivery size 412 SCTP_USE_EXT_RCVINFO use extended receive info structure (information about the next message if available) 413 SCTP_MAX_BURST number of packets that can be burst emitted 414 SCTP_CONTEXT save a context information along with the association. 415 SCTP_EXPLICIT_EOR enable sending one message across several send calls 416 SCTP_REUSE_PORT share one listening port with several sockets 417 418 read-only options: 419 SCTP_STATUS retrieve info such as number of streams, pending packets, state, ... 420 SCTP_GET_PEER_ADDR_INFO get information about a specific peer address of the association. 421 SCTP_PEER_AUTH_CHUNKS list of chunks the remote peer wants authenticated 422 SCTP_LOCAL_AUTH_CHUNKS list of chunks the local peer wants authenticated 423 SCTP_GET_ASSOC_NUMBER number of associations in a one-to-many socket 424 SCTP_GET_ASSOC_ID_LIST list of these associations 425 */ 426 427 /* In case of no_ip4, force the v6only option -- is it a valid option for SCTP ? */ 428 #ifdef IPV6_V6ONLY 429 if (fd_g_config->cnf_flags.no_ip4) { 430 int opt = 1; 431 CHECK_SYS(setsockopt(sk, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt))); 432 } 433 #endif /* IPV6_V6ONLY */ 434 435 return 0; 42 return ENOTSUP; 436 43 } 437 44 438 439 /* Post-binding socket options */ 440 static int fd_setsockopt_postbind(int sk, int bound_to_default) 45 int fd_sctp_get_str_info( int socket, int *in, int *out ) 441 46 { 442 T RACE_ENTRY( "%d %d", sk, bound_to_default);47 TODO("Retrieve streams info from the socket"); 443 48 444 CHECK_PARAMS( (sk > 0) ); 445 446 /* Set the ASCONF option */ 447 #ifdef SCTP_AUTO_ASCONF 448 { 449 int asconf; 450 451 if (TRACE_BOOL(SCTP_LEVEL)) { 452 socklen_t sz; 453 454 sz = sizeof(asconf); 455 /* Read socket defaults */ 456 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_AUTO_ASCONF, &asconf, &sz) ); 457 if (sz != sizeof(asconf)) 458 { 459 TRACE_DEBUG(INFO, "Invalid size of socket option: %d / %d", sz, (socklen_t)sizeof(asconf)); 460 return ENOTSUP; 461 } 462 fd_log_debug( "Def SCTP_AUTO_ASCONF value : %s\n", asconf ? "true" : "false"); 463 } 464 465 asconf = bound_to_default ? 1 : 0; /* allow automatic use of added or removed addresses in the association (for bound-all sockets) */ 466 467 /* Set the option to the socket */ 468 CHECK_SYS( setsockopt(sk, IPPROTO_SCTP, SCTP_AUTO_ASCONF, &asconf, sizeof(asconf)) ); 469 470 if (TRACE_BOOL(SCTP_LEVEL)) { 471 socklen_t sz = sizeof(asconf); 472 /* Check new values */ 473 CHECK_SYS( getsockopt(sk, IPPROTO_SCTP, SCTP_AUTO_ASCONF, &asconf, &sz) ); 474 fd_log_debug( "New SCTP_AUTO_ASCONF value : %s\n", asconf ? "true" : "false"); 475 } 476 } 477 #else /* SCTP_AUTO_ASCONF */ 478 TRACE_DEBUG(SCTP_LEVEL, "Skipping SCTP_AUTO_ASCONF"); 479 #endif /* SCTP_AUTO_ASCONF */ 480 481 return 0; 49 return ENOTSUP; 482 50 } 483 484 /* Create a socket server and bind it according to daemon s configuration */485 int fd_sctp_create_bind_server( int * sock, struct fd_list * list, uint16_t port )486 {487 int family;488 int bind_default;489 490 TRACE_ENTRY("%p %p %hu", sock, list, port);491 CHECK_PARAMS(sock);492 493 if (fd_g_config->cnf_flags.no_ip6) {494 family = AF_INET;495 } else {496 family = AF_INET6; /* can create socket for both IP and IPv6 */497 }498 499 /* Create the socket */500 CHECK_SYS( *sock = socket(family, SOCK_STREAM, IPPROTO_SCTP) );501 502 /* Set pre-binding socket options, including number of streams etc... */503 CHECK_FCT( fd_setsockopt_prebind(*sock) );504 505 bind_default = (! list) || (FD_IS_LIST_EMPTY(list)) ;506 redo:507 if ( bind_default ) {508 /* Implicit endpoints : bind to default addresses */509 union {510 sSS ss;511 sSA sa;512 sSA4 sin;513 sSA6 sin6;514 } s;515 516 /* 0.0.0.0 and [::] are all zeros */517 memset(&s, 0, sizeof(s));518 519 s.sa.sa_family = family;520 521 if (family == AF_INET)522 s.sin.sin_port = htons(port);523 else524 s.sin6.sin6_port = htons(port);525 526 CHECK_SYS( bind(*sock, &s.sa, sizeof(s)) );527 528 } else {529 /* Explicit endpoints to bind to from config */530 531 union {532 sSA * sa;533 sSA4 *sin;534 sSA6 *sin6;535 uint8_t *buf;536 } ptr;537 union {538 sSA * sa;539 uint8_t * buf;540 } sar;541 int count = 0; /* number of sock addr in sar array */542 size_t offset = 0;543 struct fd_list * li;544 545 sar.buf = NULL;546 547 /* Create a flat array from the list of configured addresses */548 for (li = list->next; li != list; li = li->next) {549 struct fd_endpoint * ep = (struct fd_endpoint *)li;550 size_t sz = 0;551 552 if (! (ep->flags & EP_FL_CONF))553 continue;554 555 count++;556 557 /* Size of the new SA we are adding (sar may contain a mix of sockaddr_in and sockaddr_in6) */558 #ifndef SCTP_USE_MAPPED_ADDRESSES559 if (ep->sa.sa_family == AF_INET6)560 #else /* SCTP_USE_MAPPED_ADDRESSES */561 if (family == AF_INET6)562 #endif /* SCTP_USE_MAPPED_ADDRESSES */563 sz = sizeof(sSA6);564 else565 sz = sizeof(sSA4);566 567 /* augment sar to contain the additional info */568 CHECK_MALLOC( sar.buf = realloc(sar.buf, offset + sz) );569 570 ptr.buf = sar.buf + offset; /* place of the new SA */571 offset += sz; /* update to end of sar */572 573 if (sz == sizeof(sSA4)) {574 memcpy(ptr.buf, &ep->sin, sz);575 ptr.sin->sin_port = htons(port);576 } else {577 if (ep->sa.sa_family == AF_INET) { /* We must map the address */578 memset(ptr.buf, 0, sz);579 ptr.sin6->sin6_family = AF_INET6;580 IN6_ADDR_V4MAP( &ptr.sin6->sin6_addr.s6_addr, ep->sin.sin_addr.s_addr );581 } else {582 memcpy(ptr.sin6, &ep->sin6, sz);583 }584 ptr.sin6->sin6_port = htons(port);585 }586 }587 588 if (!count) {589 /* None of the addresses in the list came from configuration, we bind to default */590 bind_default = 1;591 goto redo;592 }593 594 if (TRACE_BOOL(SCTP_LEVEL)) {595 int i;596 ptr.buf = sar.buf;597 fd_log_debug("Calling sctp_bindx with the following address array:\n");598 for (i = 0; i < count; i++) {599 TRACE_DEBUG_sSA(FULL, " - ", ptr.sa, NI_NUMERICHOST | NI_NUMERICSERV, "" );600 ptr.buf += (ptr.sa->sa_family == AF_INET) ? sizeof(sSA4) : sizeof(sSA6) ;601 }602 }603 604 /* Bind to this array */605 CHECK_SYS( sctp_bindx(*sock, sar.sa, count, SCTP_BINDX_ADD_ADDR) );606 607 /* We don't need sar anymore */608 free(sar.buf);609 }610 611 /* Now, the server is bound, set remaining sockopt */612 CHECK_FCT( fd_setsockopt_postbind(*sock, bind_default) );613 614 /* Debug: show all local listening addresses */615 if (TRACE_BOOL(SCTP_LEVEL)) {616 sSA *sar;617 union {618 sSA *sa;619 uint8_t *buf;620 } ptr;621 int sz;622 623 CHECK_SYS( sz = sctp_getladdrs(*sock, 0, &sar) );624 625 fd_log_debug("SCTP server bound on :\n");626 for (ptr.sa = sar; sz-- > 0; ptr.buf += (ptr.sa->sa_family == AF_INET) ? sizeof(sSA4) : sizeof(sSA6)) {627 TRACE_DEBUG_sSA(FULL, " - ", ptr.sa, NI_NUMERICHOST | NI_NUMERICSERV, "" );628 }629 sctp_freeladdrs(sar);630 }631 632 return 0;633 }634 635 /* Allow clients connections on server sockets */636 int fd_sctp_listen( int sock )637 {638 TRACE_ENTRY("%d", sock);639 CHECK_SYS( listen(sock, 5) );640 return 0;641 }642 643 /* Create a client socket and connect to remote server */644 int fd_sctp_client( int *sock, int no_ip6, uint16_t port, struct fd_list * list )645 {646 int family;647 int count = 0;648 size_t offset = 0, sz;649 union {650 uint8_t *buf;651 sSA *sa;652 } sar;653 union {654 uint8_t *buf;655 sSA *sa;656 sSA4 *sin;657 sSA6 *sin6;658 } ptr;659 struct fd_list * li;660 int ret;661 662 sar.buf = NULL;663 664 TRACE_ENTRY("%p %i %hu %p", sock, no_ip6, port, list);665 CHECK_PARAMS( sock && list && (!FD_IS_LIST_EMPTY(list)) );666 667 if (no_ip6) {668 family = AF_INET;669 } else {670 family = AF_INET6;671 }672 673 /* Create the socket */674 CHECK_SYS( *sock = socket(family, SOCK_STREAM, IPPROTO_SCTP) );675 676 /* Cleanup if we are cancelled */677 pthread_cleanup_push(fd_cleanup_socket, sock);678 679 /* Set the socket options */680 CHECK_FCT_DO( ret = fd_setsockopt_prebind(*sock), goto fail );681 682 /* Create the array of addresses for sctp_connectx */683 for (li = list->next; li != list; li = li->next) {684 struct fd_endpoint * ep = (struct fd_endpoint *) li;685 686 count++;687 688 /* Size of the new SA we are adding (sar may contain a mix of sockaddr_in and sockaddr_in6) */689 #ifndef SCTP_USE_MAPPED_ADDRESSES690 if (ep->sa.sa_family == AF_INET6)691 #else /* SCTP_USE_MAPPED_ADDRESSES */692 if (family == AF_INET6)693 #endif /* SCTP_USE_MAPPED_ADDRESSES */694 sz = sizeof(sSA6);695 else696 sz = sizeof(sSA4);697 698 /* augment sar to contain the additional info */699 CHECK_MALLOC_DO( sar.buf = realloc(sar.buf, offset + sz), { ret = ENOMEM; goto fail; } );700 701 ptr.buf = sar.buf + offset; /* place of the new SA */702 offset += sz; /* update to end of sar */703 704 if (sz == sizeof(sSA4)) {705 memcpy(ptr.buf, &ep->sin, sz);706 ptr.sin->sin_port = htons(port);707 } else {708 if (ep->sa.sa_family == AF_INET) { /* We must map the address */709 memset(ptr.buf, 0, sz);710 ptr.sin6->sin6_family = AF_INET6;711 IN6_ADDR_V4MAP( &ptr.sin6->sin6_addr.s6_addr, ep->sin.sin_addr.s_addr );712 } else {713 memcpy(ptr.sin6, &ep->sin6, sz);714 }715 ptr.sin6->sin6_port = htons(port);716 }717 }718 719 /* Try connecting */720 TRACE_DEBUG(FULL, "Attempting SCTP connection (%d addresses attempted)...", count);721 CHECK_SYS_DO( sctp_connectx(*sock, sar.sa, count), { ret = errno; goto fail; } );722 free(sar.buf); sar.buf = NULL;723 724 /* Set the remaining sockopts */725 CHECK_FCT_DO( ret = fd_setsockopt_postbind(*sock, 1), goto fail );726 727 /* Done! */728 pthread_cleanup_pop(0);729 return 0;730 731 fail:732 if (*sock > 0) {733 shutdown(*sock, SHUT_RDWR);734 *sock = -1;735 }736 free(sar.buf);737 return ret;738 }739 740 /* Retrieve streams information from a connected association -- optionaly provide the primary address */741 int fd_sctp_get_str_info( int sock, uint16_t *in, uint16_t *out, sSS *primary )742 {743 struct sctp_status status;744 socklen_t sz = sizeof(status);745 746 TRACE_ENTRY("%d %p %p %p", sock, in, out, primary);747 CHECK_PARAMS( (sock > 0) && in && out );748 749 /* Read the association parameters */750 memset(&status, 0, sizeof(status));751 CHECK_SYS( getsockopt(sock, IPPROTO_SCTP, SCTP_STATUS, &status, &sz) );752 if (sz != sizeof(status))753 {754 TRACE_DEBUG(INFO, "Invalid size of socket option: %d / %zd", sz, sizeof(status));755 return ENOTSUP;756 }757 if (TRACE_BOOL(SCTP_LEVEL)) {758 fd_log_debug( "SCTP_STATUS : sstat_state : %i\n" , status.sstat_state);759 fd_log_debug( " sstat_rwnd : %u\n" , status.sstat_rwnd);760 fd_log_debug( " sstat_unackdata : %hu\n", status.sstat_unackdata);761 fd_log_debug( " sstat_penddata : %hu\n", status.sstat_penddata);762 fd_log_debug( " sstat_instrms : %hu\n", status.sstat_instrms);763 fd_log_debug( " sstat_outstrms : %hu\n", status.sstat_outstrms);764 fd_log_debug( " sstat_fragmentation_point : %u\n" , status.sstat_fragmentation_point);765 fd_log_debug( " sstat_primary.spinfo_address : ");766 sSA_DUMP_NODE_SERV(&status.sstat_primary.spinfo_address, NI_NUMERICHOST | NI_NUMERICSERV );767 fd_log_debug( "\n" );768 fd_log_debug( " sstat_primary.spinfo_state : %d\n" , status.sstat_primary.spinfo_state);769 fd_log_debug( " sstat_primary.spinfo_cwnd : %u\n" , status.sstat_primary.spinfo_cwnd);770 fd_log_debug( " sstat_primary.spinfo_srtt : %u\n" , status.sstat_primary.spinfo_srtt);771 fd_log_debug( " sstat_primary.spinfo_rto : %u\n" , status.sstat_primary.spinfo_rto);772 fd_log_debug( " sstat_primary.spinfo_mtu : %u\n" , status.sstat_primary.spinfo_mtu);773 }774 775 *in = status.sstat_instrms;776 *out = status.sstat_outstrms;777 778 if (primary)779 memcpy(primary, &status.sstat_primary.spinfo_address, sizeof(sSS));780 781 return 0;782 }783 784 /* Get the list of local endpoints of the socket */785 int fd_sctp_get_local_ep(int sock, struct fd_list * list)786 {787 union {788 sSA *sa;789 uint8_t *buf;790 } ptr;791 792 sSA * data;793 int count;794 795 TRACE_ENTRY("%d %p", sock, list);796 CHECK_PARAMS(list);797 798 /* Read the list on the socket */799 CHECK_SYS( count = sctp_getladdrs(sock, 0, &data) );800 ptr.sa = data;801 802 while (count) {803 socklen_t sl;804 switch (ptr.sa->sa_family) {805 case AF_INET: sl = sizeof(sSA4); break;806 case AF_INET6: sl = sizeof(sSA6); break;807 default:808 TRACE_DEBUG(INFO, "Unkown address family returned in sctp_getladdrs: %d", ptr.sa->sa_family);809 }810 811 CHECK_FCT( fd_ep_add_merge( list, ptr.sa, sl, EP_FL_LL ) );812 ptr.buf += sl;813 count --;814 }815 816 /* Free the list */817 sctp_freeladdrs(data);818 819 /* Now get the primary address, the add function will take care of merging with existing entry */820 {821 822 struct sctp_status status;823 socklen_t sz = sizeof(status);824 int ret;825 826 memset(&status, 0, sizeof(status));827 /* Attempt to use SCTP_STATUS message to retrieve the primary address */828 ret = getsockopt(sock, IPPROTO_SCTP, SCTP_STATUS, &status, &sz);829 if (sz != sizeof(status))830 ret = -1;831 sz = sizeof(sSS);832 if (ret < 0)833 {834 /* Fallback to getsockname -- not recommended by draft-ietf-tsvwg-sctpsocket-19#section-7.4 */835 CHECK_SYS(getsockname(sock, (sSA *)&status.sstat_primary.spinfo_address, &sz));836 }837 838 CHECK_FCT( fd_ep_add_merge( list, (sSA *)&status.sstat_primary.spinfo_address, sz, EP_FL_PRIMARY ) );839 }840 841 return 0;842 }843 844 /* Get the list of remote endpoints of the socket */845 int fd_sctp_get_remote_ep(int sock, struct fd_list * list)846 {847 union {848 sSA *sa;849 uint8_t *buf;850 } ptr;851 852 sSA * data;853 int count;854 855 TRACE_ENTRY("%d %p", sock, list);856 CHECK_PARAMS(list);857 858 /* Read the list on the socket */859 CHECK_SYS( count = sctp_getpaddrs(sock, 0, &data) );860 ptr.sa = data;861 862 while (count) {863 socklen_t sl;864 switch (ptr.sa->sa_family) {865 case AF_INET: sl = sizeof(sSA4); break;866 case AF_INET6: sl = sizeof(sSA6); break;867 default:868 TRACE_DEBUG(INFO, "Unkown address family returned in sctp_getpaddrs: %d", ptr.sa->sa_family);869 }870 871 CHECK_FCT( fd_ep_add_merge( list, ptr.sa, sl, EP_FL_LL ) );872 ptr.buf += sl;873 count --;874 }875 876 /* Free the list */877 sctp_freepaddrs(data);878 879 /* Now get the primary address, the add function will take care of merging with existing entry */880 {881 sSS ss;882 socklen_t sl = sizeof(sSS);883 884 CHECK_SYS(getpeername(sock, (sSA *)&ss, &sl));885 CHECK_FCT( fd_ep_add_merge( list, (sSA *)&ss, sl, EP_FL_PRIMARY ) );886 }887 888 /* Done! */889 return 0;890 }891 892 /* Send a buffer over a specified stream */893 int fd_sctp_sendstr(int sock, uint16_t strid, uint8_t * buf, size_t len)894 {895 struct msghdr mhdr;896 struct iovec iov;897 struct {898 struct cmsghdr hdr;899 struct sctp_sndrcvinfo sndrcv;900 } anci;901 ssize_t ret;902 903 TRACE_ENTRY("%d %hu %p %zd", sock, strid, buf, len);904 905 memset(&mhdr, 0, sizeof(mhdr));906 memset(&iov, 0, sizeof(iov));907 memset(&anci, 0, sizeof(anci));908 909 /* IO Vector: message data */910 iov.iov_base = buf;911 iov.iov_len = len;912 913 /* Anciliary data: specify SCTP stream */914 anci.hdr.cmsg_len = sizeof(anci);915 anci.hdr.cmsg_level = IPPROTO_SCTP;916 anci.hdr.cmsg_type = SCTP_SNDRCV;917 anci.sndrcv.sinfo_stream = strid;918 /* note : we could store other data also, for example in .sinfo_ppid for remote peer or in .sinfo_context for errors. */919 920 /* We don't use mhdr.msg_name here; it could be used to specify an address different from the primary */921 922 mhdr.msg_iov = &iov;923 mhdr.msg_iovlen = 1;924 925 mhdr.msg_control = &anci;926 mhdr.msg_controllen = sizeof(anci);927 928 TRACE_DEBUG(FULL, "Sending %db data on stream %hu of socket %d", len, strid, sock);929 930 CHECK_SYS( ret = sendmsg(sock, &mhdr, 0) );931 ASSERT( ret == len ); /* There should not be partial delivery with sendmsg... */932 933 return 0;934 }935 936 /* Receive the next data from the socket, or next notification */937 int fd_sctp_recvmeta(int sock, uint16_t * strid, uint8_t ** buf, size_t * len, int *event)938 {939 ssize_t ret = 0;940 struct msghdr mhdr;941 char ancidata[ CMSG_BUF_LEN ];942 struct iovec iov;943 uint8_t *data = NULL;944 size_t bufsz = 0, datasize = 0;945 size_t mempagesz = sysconf(_SC_PAGESIZE); /* We alloc buffer by memory pages for efficiency */946 947 TRACE_ENTRY("%d %p %p %p %p", sock, strid, buf, len, event);948 CHECK_PARAMS( (sock > 0) && buf && len && event );949 950 /* Cleanup out parameters */951 *buf = NULL;952 *len = 0;953 *event = 0;954 955 /* Prepare header for receiving message */956 memset(&mhdr, 0, sizeof(mhdr));957 mhdr.msg_iov = &iov;958 mhdr.msg_iovlen = 1;959 mhdr.msg_control = &ancidata;960 mhdr.msg_controllen = sizeof(ancidata);961 962 /* We will loop while all data is not received. */963 incomplete:964 if (datasize == bufsz) {965 /* The buffer is full, enlarge it */966 bufsz += mempagesz;967 CHECK_MALLOC( data = realloc(data, bufsz) );968 }969 /* the new data will be received following the preceding */970 memset(&iov, 0, sizeof(iov));971 iov.iov_base = data + datasize ;972 iov.iov_len = bufsz - datasize;973 974 /* Receive data from the socket */975 pthread_cleanup_push(free, data);976 ret = recvmsg(sock, &mhdr, 0);977 pthread_cleanup_pop(0);978 979 /* Handle errors */980 if (ret <= 0) { /* Socket is closed, or an error occurred */981 CHECK_SYS_DO(ret, /* to log in case of error */);982 free(data);983 *event = FDEVP_CNX_ERROR;984 return 0;985 }986 987 /* Update the size of data we received */988 datasize += ret;989 990 /* SCTP provides an indication when we received a full record; loop if it is not the case */991 if ( ! (mhdr.msg_flags & MSG_EOR) ) {992 goto incomplete;993 }994 995 TRACE_DEBUG(FULL, "Received %db data on socket %d", datasize, sock);996 997 /* Handle the case where the data received is a notification */998 if (mhdr.msg_flags & MSG_NOTIFICATION) {999 union sctp_notification * notif = (union sctp_notification *) data;1000 1001 switch (notif->sn_header.sn_type) {1002 1003 case SCTP_ASSOC_CHANGE:1004 TRACE_DEBUG(FULL, "Received SCTP_ASSOC_CHANGE notification");1005 TRACE_DEBUG(SCTP_LEVEL, " state : %hu", notif->sn_assoc_change.sac_state);1006 TRACE_DEBUG(SCTP_LEVEL, " error : %hu", notif->sn_assoc_change.sac_error);1007 TRACE_DEBUG(SCTP_LEVEL, " instr : %hu", notif->sn_assoc_change.sac_inbound_streams);1008 TRACE_DEBUG(SCTP_LEVEL, " outstr : %hu", notif->sn_assoc_change.sac_outbound_streams);1009 1010 *event = FDEVP_CNX_EP_CHANGE;1011 break;1012 1013 case SCTP_PEER_ADDR_CHANGE:1014 TRACE_DEBUG(FULL, "Received SCTP_PEER_ADDR_CHANGE notification");1015 TRACE_DEBUG_sSA(SCTP_LEVEL, " intf_change : ", &(notif->sn_paddr_change.spc_aaddr), NI_NUMERICHOST | NI_NUMERICSERV, "" );1016 TRACE_DEBUG(SCTP_LEVEL, " state : %d", notif->sn_paddr_change.spc_state);1017 TRACE_DEBUG(SCTP_LEVEL, " error : %d", notif->sn_paddr_change.spc_error);1018 1019 *event = FDEVP_CNX_EP_CHANGE;1020 break;1021 1022 case SCTP_SEND_FAILED:1023 TRACE_DEBUG(FULL, "Received SCTP_SEND_FAILED notification");1024 TRACE_DEBUG(SCTP_LEVEL, " len : %hu", notif->sn_send_failed.ssf_length);1025 TRACE_DEBUG(SCTP_LEVEL, " err : %d", notif->sn_send_failed.ssf_error);1026 1027 *event = FDEVP_CNX_ERROR;1028 break;1029 1030 case SCTP_REMOTE_ERROR:1031 TRACE_DEBUG(FULL, "Received SCTP_REMOTE_ERROR notification");1032 TRACE_DEBUG(SCTP_LEVEL, " err : %hu", ntohs(notif->sn_remote_error.sre_error));1033 TRACE_DEBUG(SCTP_LEVEL, " len : %hu", ntohs(notif->sn_remote_error.sre_length));1034 1035 *event = FDEVP_CNX_ERROR;1036 break;1037 1038 case SCTP_SHUTDOWN_EVENT:1039 TRACE_DEBUG(FULL, "Received SCTP_SHUTDOWN_EVENT notification");1040 1041 *event = FDEVP_CNX_ERROR;1042 break;1043 1044 default:1045 TRACE_DEBUG(FULL, "Received unknown notification %d, assume error", notif->sn_header.sn_type);1046 *event = FDEVP_CNX_ERROR;1047 }1048 1049 free(data);1050 return 0;1051 }1052 1053 /* From this point, we have received a message */1054 *event = FDEVP_CNX_MSG_RECV;1055 *buf = data;1056 *len = datasize;1057 1058 if (strid) {1059 struct cmsghdr *hdr;1060 struct sctp_sndrcvinfo *sndrcv;1061 1062 /* Handle the anciliary data */1063 for (hdr = CMSG_FIRSTHDR(&mhdr); hdr; hdr = CMSG_NXTHDR(&mhdr, hdr)) {1064 1065 /* We deal only with anciliary data at SCTP level */1066 if (hdr->cmsg_level != IPPROTO_SCTP) {1067 TRACE_DEBUG(FULL, "Received some anciliary data at level %d, skipped", hdr->cmsg_level);1068 continue;1069 }1070 1071 /* Also only interested in SCTP_SNDRCV message for the moment */1072 if (hdr->cmsg_type != SCTP_SNDRCV) {1073 TRACE_DEBUG(FULL, "Anciliary block IPPROTO_SCTP / %d, skipped", hdr->cmsg_type);1074 continue;1075 }1076 1077 sndrcv = (struct sctp_sndrcvinfo *) CMSG_DATA(hdr);1078 if (TRACE_BOOL(SCTP_LEVEL)) {1079 fd_log_debug( "Anciliary block IPPROTO_SCTP / SCTP_SNDRCV\n");1080 fd_log_debug( " sinfo_stream : %hu\n", sndrcv->sinfo_stream);1081 fd_log_debug( " sinfo_ssn : %hu\n", sndrcv->sinfo_ssn);1082 fd_log_debug( " sinfo_flags : %hu\n", sndrcv->sinfo_flags);1083 /* fd_log_debug( " sinfo_pr_policy : %hu\n", sndrcv->sinfo_pr_policy); */1084 fd_log_debug( " sinfo_ppid : %u\n" , sndrcv->sinfo_ppid);1085 fd_log_debug( " sinfo_context : %u\n" , sndrcv->sinfo_context);1086 /* fd_log_debug( " sinfo_pr_value : %u\n" , sndrcv->sinfo_pr_value); */1087 fd_log_debug( " sinfo_tsn : %u\n" , sndrcv->sinfo_tsn);1088 fd_log_debug( " sinfo_cumtsn : %u\n" , sndrcv->sinfo_cumtsn);1089 }1090 1091 *strid = sndrcv->sinfo_stream;1092 }1093 }1094 1095 return 0;1096 }
Note: See TracChangeset
for help on using the changeset viewer.