1 /* 2 * cachedb/redis.c - cachedb redis module 3 * 4 * Copyright (c) 2018, NLnet Labs. All rights reserved. 5 * 6 * This software is open source. 7 * 8 * Redistribution and use in source and binary forms, with or without 9 * modification, are permitted provided that the following conditions 10 * are met: 11 * 12 * Redistributions of source code must retain the above copyright notice, 13 * this list of conditions and the following disclaimer. 14 * 15 * Redistributions in binary form must reproduce the above copyright notice, 16 * this list of conditions and the following disclaimer in the documentation 17 * and/or other materials provided with the distribution. 18 * 19 * Neither the name of the NLNET LABS nor the names of its contributors may 20 * be used to endorse or promote products derived from this software without 21 * specific prior written permission. 22 * 23 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 24 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 25 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 26 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 29 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 30 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 31 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 32 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 33 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 */ 35 36 /** 37 * \file 38 * 39 * This file contains a module that uses the redis database to cache 40 * dns responses. 41 */ 42 43 #include "config.h" 44 #ifdef USE_CACHEDB 45 #include "cachedb/redis.h" 46 #include "cachedb/cachedb.h" 47 #include "util/alloc.h" 48 #include "util/config_file.h" 49 #include "util/locks.h" 50 #include "util/timeval_func.h" 51 #include "sldns/sbuffer.h" 52 53 #ifdef USE_REDIS 54 #include "hiredis/hiredis.h" 55 56 struct redis_moddata { 57 /* thread-specific redis contexts */ 58 redisContext** ctxs; 59 redisContext** replica_ctxs; 60 /* number of ctx entries */ 61 int numctxs; 62 /* server's IP address or host name */ 63 const char* server_host; 64 const char* replica_server_host; 65 /* server's TCP port */ 66 int server_port; 67 int replica_server_port; 68 /* server's unix path, or "", NULL if unused */ 69 const char* server_path; 70 const char* replica_server_path; 71 /* server's AUTH password, or "", NULL if unused */ 72 const char* server_password; 73 const char* replica_server_password; 74 /* timeout for commands */ 75 struct timeval command_timeout; 76 struct timeval replica_command_timeout; 77 /* timeout for connection setup */ 78 struct timeval connect_timeout; 79 struct timeval replica_connect_timeout; 80 /* the reconnect interval time. */ 81 struct timeval reconnect_interval; 82 struct timeval replica_reconnect_interval; 83 /* reconnect attempts, 0 if connected, counts up failed reconnects. */ 84 int reconnect_attempts; 85 int replica_reconnect_attempts; 86 /* Lock on reconnect_wait time. */ 87 lock_basic_type wait_lock; 88 lock_basic_type replica_wait_lock; 89 /* reconnect wait time, wait until it has passed before reconnect. */ 90 struct timeval reconnect_wait; 91 struct timeval replica_reconnect_wait; 92 /* the redis logical database to use */ 93 int logical_db; 94 int replica_logical_db; 95 /* if the SET with EX command is supported */ 96 int set_with_ex_available; 97 }; 98 99 /** The limit on the number of redis connect attempts. After failure if 100 * the number is exceeded, the reconnects are throttled by the wait time. */ 101 #define REDIS_RECONNECT_ATTEMPT_LIMIT 3 102 103 static redisReply* redis_command(struct module_env*, struct cachedb_env*, 104 const char*, const uint8_t*, size_t, int); 105 106 static void 107 moddata_clean(struct redis_moddata** moddata) { 108 if(!moddata || !*moddata) 109 return; 110 if((*moddata)->ctxs) { 111 int i; 112 for(i = 0; i < (*moddata)->numctxs; i++) { 113 if((*moddata)->ctxs[i]) 114 redisFree((*moddata)->ctxs[i]); 115 } 116 free((*moddata)->ctxs); 117 } 118 if((*moddata)->replica_ctxs) { 119 int i; 120 for(i = 0; i < (*moddata)->numctxs; i++) { 121 if((*moddata)->replica_ctxs[i]) 122 redisFree((*moddata)->replica_ctxs[i]); 123 } 124 free((*moddata)->replica_ctxs); 125 } 126 lock_basic_destroy(&(*moddata)->wait_lock); 127 lock_basic_destroy(&(*moddata)->replica_wait_lock); 128 free(*moddata); 129 *moddata = NULL; 130 } 131 132 static redisContext* 133 redis_connect(const char* host, int port, const char* path, 134 const char* password, int logical_db, 135 const struct timeval connect_timeout, 136 const struct timeval command_timeout, 137 const struct timeval* reconnect_interval, 138 int* reconnect_attempts, 139 struct timeval* reconnect_wait, 140 lock_basic_type* wait_lock, 141 struct timeval* now_tv, 142 const char* infostr) 143 { 144 struct timeval now_val; 145 redisContext* ctx; 146 147 /* See if the redis server is down, and reconnect has to wait. */ 148 if(*reconnect_attempts > REDIS_RECONNECT_ATTEMPT_LIMIT) { 149 /* Acquire lock to look at timeval, the integer has atomic 150 * integrity. */ 151 struct timeval wait_tv; 152 if(now_tv) { 153 now_val = *now_tv; 154 } else { 155 if(gettimeofday(&now_val, NULL) < 0) 156 log_err("redis: gettimeofday: %s", 157 strerror(errno)); 158 } 159 lock_basic_lock(wait_lock); 160 wait_tv = *reconnect_wait; 161 lock_basic_unlock(wait_lock); 162 if(timeval_smaller(&now_val, &wait_tv)) { 163 verbose(VERB_ALGO, "redis %sdown, reconnect wait", 164 infostr); 165 return NULL; 166 } 167 } 168 169 if(path && path[0]!=0) { 170 ctx = redisConnectUnixWithTimeout(path, connect_timeout); 171 } else { 172 ctx = redisConnectWithTimeout(host, port, connect_timeout); 173 } 174 if(!ctx || ctx->err) { 175 const char *errstr = "out of memory"; 176 if(ctx) 177 errstr = ctx->errstr; 178 log_err("failed to connect to redis %sserver: %s", infostr, errstr); 179 goto fail; 180 } 181 if(redisSetTimeout(ctx, command_timeout) != REDIS_OK) { 182 log_err("failed to set redis %stimeout, %s", infostr, ctx->errstr); 183 goto fail; 184 } 185 if(password && password[0]!=0) { 186 redisReply* rep; 187 rep = redisCommand(ctx, "AUTH %s", password); 188 if(!rep || rep->type == REDIS_REPLY_ERROR) { 189 log_err("failed to authenticate %swith password", infostr); 190 freeReplyObject(rep); 191 goto fail; 192 } 193 freeReplyObject(rep); 194 } 195 if(logical_db > 0) { 196 redisReply* rep; 197 rep = redisCommand(ctx, "SELECT %d", logical_db); 198 if(!rep || rep->type == REDIS_REPLY_ERROR) { 199 log_err("failed %sto set logical database (%d)", 200 infostr, logical_db); 201 freeReplyObject(rep); 202 goto fail; 203 } 204 freeReplyObject(rep); 205 } 206 *reconnect_attempts = 0; 207 if(verbosity >= VERB_OPS) { 208 char port_str[6+1]; 209 port_str[0] = ' '; 210 (void)snprintf(port_str+1, sizeof(port_str)-1, "%d", port); 211 verbose(VERB_OPS, "Connection to Redis %sestablished (%s%s)", 212 infostr, 213 path&&path[0]!=0?path:host, 214 path&&path[0]!=0?"":port_str); 215 } 216 return ctx; 217 218 fail: 219 if(ctx) 220 redisFree(ctx); 221 (*reconnect_attempts)++; 222 if(*reconnect_attempts > REDIS_RECONNECT_ATTEMPT_LIMIT) { 223 /* Wait for the reconnect interval before trying again. */ 224 struct timeval tv; 225 if(now_tv) { 226 now_val = *now_tv; 227 } else { 228 if(gettimeofday(&now_val, NULL) < 0) 229 log_err("redis: gettimeofday: %s", 230 strerror(errno)); 231 } 232 tv = now_val; 233 timeval_add(&tv, reconnect_interval); 234 lock_basic_lock(wait_lock); 235 *reconnect_wait = tv; 236 lock_basic_unlock(wait_lock); 237 verbose(VERB_ALGO, "redis %sreconnect wait until %d.%6.6d", 238 infostr, (int)tv.tv_sec, (int)tv.tv_usec); 239 } 240 return NULL; 241 } 242 243 static void 244 set_timeout(struct timeval* timeout, int value, int explicit_value) 245 { 246 int v = explicit_value != 0 ? explicit_value : value; 247 timeout->tv_sec = v / 1000; 248 timeout->tv_usec = (v % 1000) * 1000; 249 } 250 251 static int 252 redis_init(struct module_env* env, struct cachedb_env* cachedb_env) 253 { 254 int i; 255 struct redis_moddata* moddata = NULL; 256 257 verbose(VERB_OPS, "Redis initialization"); 258 259 moddata = calloc(1, sizeof(struct redis_moddata)); 260 if(!moddata) { 261 log_err("out of memory"); 262 goto fail; 263 } 264 lock_basic_init(&moddata->wait_lock); 265 lock_protect(&moddata->wait_lock, &moddata->reconnect_wait, 266 sizeof(moddata->reconnect_wait)); 267 lock_basic_init(&moddata->replica_wait_lock); 268 lock_protect(&moddata->replica_wait_lock, 269 &moddata->replica_reconnect_wait, 270 sizeof(moddata->replica_reconnect_wait)); 271 moddata->numctxs = env->cfg->num_threads; 272 /* note: server_host and similar string configuration options are 273 * shallow references to configured strings; we don't have to free them 274 * in this module. */ 275 moddata->server_host = env->cfg->redis_server_host; 276 moddata->replica_server_host = env->cfg->redis_replica_server_host; 277 278 moddata->server_port = env->cfg->redis_server_port; 279 moddata->replica_server_port = env->cfg->redis_replica_server_port; 280 281 moddata->server_path = env->cfg->redis_server_path; 282 moddata->replica_server_path = env->cfg->redis_replica_server_path; 283 284 moddata->server_password = env->cfg->redis_server_password; 285 moddata->replica_server_password = env->cfg->redis_replica_server_password; 286 287 set_timeout(&moddata->command_timeout, 288 env->cfg->redis_timeout, 289 env->cfg->redis_command_timeout); 290 set_timeout(&moddata->replica_command_timeout, 291 env->cfg->redis_replica_timeout, 292 env->cfg->redis_replica_command_timeout); 293 set_timeout(&moddata->connect_timeout, 294 env->cfg->redis_timeout, 295 env->cfg->redis_connect_timeout); 296 set_timeout(&moddata->replica_connect_timeout, 297 env->cfg->redis_replica_timeout, 298 env->cfg->redis_replica_connect_timeout); 299 set_timeout(&moddata->reconnect_interval, 1000, 0); 300 set_timeout(&moddata->replica_reconnect_interval, 1000, 0); 301 302 moddata->logical_db = env->cfg->redis_logical_db; 303 moddata->replica_logical_db = env->cfg->redis_replica_logical_db; 304 305 moddata->ctxs = calloc(env->cfg->num_threads, sizeof(redisContext*)); 306 if(!moddata->ctxs) { 307 log_err("out of memory"); 308 goto fail; 309 } 310 if((moddata->replica_server_host && moddata->replica_server_host[0]!=0) 311 || (moddata->replica_server_path && moddata->replica_server_path[0]!=0)) { 312 /* There is a replica configured, allocate ctxs */ 313 moddata->replica_ctxs = calloc(env->cfg->num_threads, sizeof(redisContext*)); 314 if(!moddata->replica_ctxs) { 315 log_err("out of memory"); 316 goto fail; 317 } 318 } 319 for(i = 0; i < moddata->numctxs; i++) { 320 redisContext* ctx = redis_connect( 321 moddata->server_host, 322 moddata->server_port, 323 moddata->server_path, 324 moddata->server_password, 325 moddata->logical_db, 326 moddata->connect_timeout, 327 moddata->command_timeout, 328 &moddata->reconnect_interval, 329 &moddata->reconnect_attempts, 330 &moddata->reconnect_wait, 331 &moddata->wait_lock, 332 env->now_tv, 333 ""); 334 if(!ctx) { 335 log_err("redis_init: failed to init redis " 336 "(for thread %d)", i); 337 /* And continue, the context can be established 338 * later, just like after a disconnect. */ 339 } 340 moddata->ctxs[i] = ctx; 341 } 342 if(moddata->replica_ctxs) { 343 for(i = 0; i < moddata->numctxs; i++) { 344 redisContext* ctx = redis_connect( 345 moddata->replica_server_host, 346 moddata->replica_server_port, 347 moddata->replica_server_path, 348 moddata->replica_server_password, 349 moddata->replica_logical_db, 350 moddata->replica_connect_timeout, 351 moddata->replica_command_timeout, 352 &moddata->replica_reconnect_interval, 353 &moddata->replica_reconnect_attempts, 354 &moddata->replica_reconnect_wait, 355 &moddata->replica_wait_lock, 356 env->now_tv, 357 "replica "); 358 if(!ctx) { 359 log_err("redis_init: failed to init redis " 360 "replica (for thread %d)", i); 361 /* And continue, the context can be established 362 * later, just like after a disconnect. */ 363 } 364 moddata->replica_ctxs[i] = ctx; 365 } 366 } 367 cachedb_env->backend_data = moddata; 368 if(env->cfg->redis_expire_records && 369 moddata->ctxs[env->alloc->thread_num] != NULL) { 370 redisReply* rep = NULL; 371 int redis_reply_type = 0; 372 /** check if set with ex command is supported */ 373 rep = redis_command(env, cachedb_env, 374 "SET __UNBOUND_REDIS_CHECK__ none EX 1", NULL, 0, 1); 375 if(!rep) { 376 /** init failed, no response from redis server*/ 377 goto set_with_ex_fail; 378 } 379 redis_reply_type = rep->type; 380 freeReplyObject(rep); 381 switch(redis_reply_type) { 382 case REDIS_REPLY_STATUS: 383 break; 384 default: 385 /** init failed, set_with_ex command not supported */ 386 goto set_with_ex_fail; 387 } 388 moddata->set_with_ex_available = 1; 389 } 390 return 1; 391 392 set_with_ex_fail: 393 log_err("redis_init: failure during redis_init, the " 394 "redis-expire-records option requires the SET with EX command " 395 "(redis >= 2.6.12)"); 396 return 1; 397 fail: 398 moddata_clean(&moddata); 399 return 0; 400 } 401 402 static void 403 redis_deinit(struct module_env* env, struct cachedb_env* cachedb_env) 404 { 405 struct redis_moddata* moddata = (struct redis_moddata*) 406 cachedb_env->backend_data; 407 (void)env; 408 409 verbose(VERB_OPS, "Redis deinitialization"); 410 moddata_clean(&moddata); 411 } 412 413 /* 414 * Send a redis command and get a reply. Unified so that it can be used for 415 * both SET and GET. If 'data' is non-NULL the command is supposed to be 416 * SET and GET otherwise, but the implementation of this function is agnostic 417 * about the semantics (except for logging): 'command', 'data', and 'data_len' 418 * are opaquely passed to redisCommand(). 419 * This function first checks whether a connection with a redis server has 420 * been established; if not it tries to set up a new one. 421 * It returns redisReply returned from redisCommand() or NULL if some low 422 * level error happens. The caller is responsible to check the return value, 423 * if it's non-NULL, it has to free it with freeReplyObject(). 424 */ 425 static redisReply* 426 redis_command(struct module_env* env, struct cachedb_env* cachedb_env, 427 const char* command, const uint8_t* data, size_t data_len, int write) 428 { 429 redisContext* ctx, **ctx_selector; 430 redisReply* rep; 431 struct redis_moddata* d = (struct redis_moddata*) 432 cachedb_env->backend_data; 433 434 /* We assume env->alloc->thread_num is a unique ID for each thread 435 * in [0, num-of-threads). We could treat it as an error condition 436 * if the assumption didn't hold, but it seems to be a fundamental 437 * assumption throughout the unbound architecture, so we simply assert 438 * it. */ 439 log_assert(env->alloc->thread_num < d->numctxs); 440 441 ctx_selector = !write && d->replica_ctxs 442 ?d->replica_ctxs 443 :d->ctxs; 444 ctx = ctx_selector[env->alloc->thread_num]; 445 446 /* If we've not established a connection to the server or we've closed 447 * it on a failure, try to re-establish a new one. Failures will be 448 * logged in redis_connect(). */ 449 if(!ctx) { 450 if(!write && d->replica_ctxs) { 451 ctx = redis_connect( 452 d->replica_server_host, 453 d->replica_server_port, 454 d->replica_server_path, 455 d->replica_server_password, 456 d->replica_logical_db, 457 d->replica_connect_timeout, 458 d->replica_command_timeout, 459 &d->replica_reconnect_interval, 460 &d->replica_reconnect_attempts, 461 &d->replica_reconnect_wait, 462 &d->replica_wait_lock, 463 env->now_tv, 464 "replica "); 465 } else { 466 ctx = redis_connect( 467 d->server_host, 468 d->server_port, 469 d->server_path, 470 d->server_password, 471 d->logical_db, 472 d->connect_timeout, 473 d->command_timeout, 474 &d->reconnect_interval, 475 &d->reconnect_attempts, 476 &d->reconnect_wait, 477 &d->wait_lock, 478 env->now_tv, 479 ""); 480 } 481 ctx_selector[env->alloc->thread_num] = ctx; 482 } 483 if(!ctx) return NULL; 484 485 /* Send the command and get a reply, synchronously. */ 486 rep = (redisReply*)redisCommand(ctx, command, data, data_len); 487 if(!rep) { 488 /* Once an error as a NULL-reply is returned the context cannot 489 * be reused and we'll need to set up a new connection. */ 490 log_err("redis_command: failed to receive a reply, " 491 "closing connection: %s", ctx->errstr); 492 redisFree(ctx); 493 ctx_selector[env->alloc->thread_num] = NULL; 494 return NULL; 495 } 496 497 /* Check error in reply to unify logging in that case. 498 * The caller may perform context-dependent checks and logging. */ 499 if(rep->type == REDIS_REPLY_ERROR) 500 log_err("redis: %s resulted in an error: %s", 501 data ? "set" : "get", rep->str); 502 503 return rep; 504 } 505 506 static int 507 redis_lookup(struct module_env* env, struct cachedb_env* cachedb_env, 508 char* key, struct sldns_buffer* result_buffer) 509 { 510 redisReply* rep; 511 /* Supported commands: 512 * - "GET " + key 513 */ 514 #define REDIS_LOOKUP_MAX_BUF_LEN \ 515 4 /* "GET " */ \ 516 +(CACHEDB_HASHSIZE/8)*2 /* key hash */ \ 517 + 1 /* \0 */ 518 char cmdbuf[REDIS_LOOKUP_MAX_BUF_LEN]; 519 int n; 520 int ret = 0; 521 522 verbose(VERB_ALGO, "redis_lookup of %s", key); 523 524 n = snprintf(cmdbuf, sizeof(cmdbuf), "GET %s", key); 525 if(n < 0 || n >= (int)sizeof(cmdbuf)) { 526 log_err("redis_lookup: unexpected failure to build command"); 527 return 0; 528 } 529 530 rep = redis_command(env, cachedb_env, cmdbuf, NULL, 0, 0); 531 if(!rep) 532 return 0; 533 switch(rep->type) { 534 case REDIS_REPLY_NIL: 535 verbose(VERB_ALGO, "redis_lookup: no data cached"); 536 break; 537 case REDIS_REPLY_STRING: 538 verbose(VERB_ALGO, "redis_lookup found %d bytes", 539 (int)rep->len); 540 if((size_t)rep->len > sldns_buffer_capacity(result_buffer)) { 541 log_err("redis_lookup: replied data too long: %lu", 542 (size_t)rep->len); 543 break; 544 } 545 sldns_buffer_clear(result_buffer); 546 sldns_buffer_write(result_buffer, rep->str, rep->len); 547 sldns_buffer_flip(result_buffer); 548 ret = 1; 549 break; 550 case REDIS_REPLY_ERROR: 551 break; /* already logged */ 552 default: 553 log_err("redis_lookup: unexpected type of reply for (%d)", 554 rep->type); 555 break; 556 } 557 freeReplyObject(rep); 558 return ret; 559 } 560 561 static void 562 redis_store(struct module_env* env, struct cachedb_env* cachedb_env, 563 char* key, uint8_t* data, size_t data_len, time_t ttl) 564 { 565 redisReply* rep; 566 int n; 567 struct redis_moddata* moddata = (struct redis_moddata*) 568 cachedb_env->backend_data; 569 int set_ttl = (moddata->set_with_ex_available && 570 env->cfg->redis_expire_records && 571 (!env->cfg->serve_expired || env->cfg->serve_expired_ttl > 0)); 572 /* Supported commands: 573 * - "SET " + key + " %b" 574 * - "SET " + key + " %b EX " + ttl 575 * older redis 2.0.0 was "SETEX " + key + " " + ttl + " %b" 576 * - "EXPIRE " + key + " 0" 577 */ 578 #define REDIS_STORE_MAX_BUF_LEN \ 579 7 /* "EXPIRE " */ \ 580 +(CACHEDB_HASHSIZE/8)*2 /* key hash */ \ 581 + 7 /* " %b EX " */ \ 582 + 20 /* ttl (uint64_t) */ \ 583 + 1 /* \0 */ 584 char cmdbuf[REDIS_STORE_MAX_BUF_LEN]; 585 586 if (!set_ttl) { 587 verbose(VERB_ALGO, "redis_store %s (%d bytes)", key, (int)data_len); 588 /* build command to set to a binary safe string */ 589 n = snprintf(cmdbuf, sizeof(cmdbuf), "SET %s %%b", key); 590 } else if(ttl == 0) { 591 /* use the EXPIRE command, SET with EX 0 is an invalid time. */ 592 /* Replies with REDIS_REPLY_INTEGER of 1. */ 593 verbose(VERB_ALGO, "redis_store expire %s (%d bytes)", 594 key, (int)data_len); 595 n = snprintf(cmdbuf, sizeof(cmdbuf), "EXPIRE %s 0", key); 596 data = NULL; 597 data_len = 0; 598 } else { 599 /* add expired ttl time to redis ttl to avoid premature eviction of key */ 600 ttl += env->cfg->serve_expired_ttl; 601 verbose(VERB_ALGO, "redis_store %s (%d bytes) with ttl %u", 602 key, (int)data_len, (unsigned)(uint32_t)ttl); 603 /* build command to set to a binary safe string */ 604 n = snprintf(cmdbuf, sizeof(cmdbuf), "SET %s %%b EX %u", key, 605 (unsigned)(uint32_t)ttl); 606 } 607 608 609 if(n < 0 || n >= (int)sizeof(cmdbuf)) { 610 log_err("redis_store: unexpected failure to build command"); 611 return; 612 } 613 614 rep = redis_command(env, cachedb_env, cmdbuf, data, data_len, 1); 615 if(rep) { 616 verbose(VERB_ALGO, "redis_store set completed"); 617 if(rep->type != REDIS_REPLY_STATUS && 618 rep->type != REDIS_REPLY_ERROR && 619 rep->type != REDIS_REPLY_INTEGER) { 620 log_err("redis_store: unexpected type of reply (%d)", 621 rep->type); 622 } 623 freeReplyObject(rep); 624 } 625 } 626 627 struct cachedb_backend redis_backend = { "redis", 628 redis_init, redis_deinit, redis_lookup, redis_store 629 }; 630 #endif /* USE_REDIS */ 631 #endif /* USE_CACHEDB */ 632