- add connection names to connections
[strongswan.git] / Source / charon / threads / stroke_interface.c
1 /**
2 * @file stroke.c
3 *
4 * @brief Implementation of stroke_t.
5 *
6 */
7
8 /*
9 * Copyright (C) 2006 Martin Willi
10 * Hochschule fuer Technik Rapperswil
11 *
12 * This program is free software; you can redistribute it and/or modify it
13 * under the terms of the GNU General Public License as published by the
14 * Free Software Foundation; either version 2 of the License, or (at your
15 * option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
19 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
20 * for more details.
21 */
22
23 #include <stdlib.h>
24 #include <sys/types.h>
25 #include <sys/stat.h>
26 #include <sys/socket.h>
27 #include <sys/un.h>
28 #include <sys/fcntl.h>
29 #include <unistd.h>
30 #include <dirent.h>
31 #include <errno.h>
32 #include <pthread.h>
33
34 #include "stroke_interface.h"
35
36 #include <stroke.h>
37 #include <types.h>
38 #include <daemon.h>
39 #include <crypto/x509.h>
40 #include <queues/jobs/initiate_ike_sa_job.h>
41
42
43 struct sockaddr_un socket_addr = { AF_UNIX, STROKE_SOCKET};
44
45
46 typedef struct private_stroke_t private_stroke_t;
47
48 /**
49 * Private data of an stroke_t object.
50 */
51 struct private_stroke_t {
52
53 /**
54 * Public part of stroke_t object.
55 */
56 stroke_t public;
57
58 /**
59 * Assigned logger_t object in charon.
60 */
61 logger_t *logger;
62
63 /**
64 * Logger which logs to stroke
65 */
66 logger_t *stroke_logger;
67
68 /**
69 * Unix socket to listen for strokes
70 */
71 int socket;
72
73 /**
74 * Thread which reads from the socket
75 */
76 pthread_t assigned_thread;
77
78 /**
79 * Read from the socket and handle stroke messages
80 */
81 void (*stroke_receive) (private_stroke_t *this);
82 };
83
84 /**
85 * Helper function which corrects the string pointers
86 * in a stroke_msg_t. Strings in a stroke_msg sent over "wire"
87 * contains RELATIVE addresses (relative to the beginning of the
88 * stroke_msg). They must be corrected if they reach our address
89 * space...
90 */
91 static void pop_string(stroke_msg_t *msg, char **string)
92 {
93 /* check for sanity of string pointer and string */
94 if (*string == NULL)
95 {
96 *string = "";
97 }
98 else if (string < (char**)msg ||
99 string > (char**)msg + sizeof(stroke_msg_t) ||
100 *string < (char*)msg->buffer - (u_int)msg ||
101 *string > (char*)(u_int)msg->length)
102 {
103 *string = "(invalid char* in stroke msg)";
104 }
105 else
106 {
107 *string = (char*)msg + (u_int)*string;
108 }
109 }
110
111 /**
112 * Add a connection to the configuration list
113 */
114 static void stroke_add_conn(private_stroke_t *this, stroke_msg_t *msg)
115 {
116 connection_t *connection;
117 policy_t *policy;
118 identification_t *my_id, *other_id;
119 host_t *my_host, *other_host, *my_subnet, *other_subnet;
120 proposal_t *proposal;
121 traffic_selector_t *my_ts, *other_ts;
122 x509_t *cert;
123
124 pop_string(msg, &msg->add_conn.name);
125 pop_string(msg, &msg->add_conn.me.address);
126 pop_string(msg, &msg->add_conn.other.address);
127 pop_string(msg, &msg->add_conn.me.id);
128 pop_string(msg, &msg->add_conn.other.id);
129 pop_string(msg, &msg->add_conn.me.cert);
130 pop_string(msg, &msg->add_conn.other.cert);
131 pop_string(msg, &msg->add_conn.me.subnet);
132 pop_string(msg, &msg->add_conn.other.subnet);
133
134 this->logger->log(this->logger, CONTROL, "received stroke: add connection \"%s\"", msg->add_conn.name);
135
136 my_host = host_create(AF_INET, msg->add_conn.me.address, 500);
137 if (my_host == NULL)
138 {
139 this->stroke_logger->log(this->stroke_logger, ERROR, "invalid host: %s", msg->add_conn.me.address);
140 return;
141 }
142 other_host = host_create(AF_INET, msg->add_conn.other.address, 500);
143 if (other_host == NULL)
144 {
145 this->stroke_logger->log(this->stroke_logger, ERROR, "invalid host: %s", msg->add_conn.other.address);
146 my_host->destroy(my_host);
147 return;
148 }
149 my_id = identification_create_from_string(*msg->add_conn.me.id ?
150 msg->add_conn.me.id : msg->add_conn.me.address);
151 if (my_id == NULL)
152 {
153 this->stroke_logger->log(this->stroke_logger, ERROR, "invalid id: %s", msg->add_conn.me.id);
154 my_host->destroy(my_host);
155 other_host->destroy(other_host);
156 return;
157 }
158 other_id = identification_create_from_string(*msg->add_conn.other.id ?
159 msg->add_conn.other.id : msg->add_conn.other.address);
160 if (other_id == NULL)
161 {
162 my_host->destroy(my_host);
163 other_host->destroy(other_host);
164 my_id->destroy(my_id);
165 this->stroke_logger->log(this->stroke_logger, ERROR, "invalid id: %s", msg->add_conn.other.id);
166 return;
167 }
168
169 my_subnet = host_create(AF_INET, *msg->add_conn.me.subnet ? msg->add_conn.me.subnet : msg->add_conn.me.address, 500);
170 if (my_subnet == NULL)
171 {
172 my_host->destroy(my_host);
173 other_host->destroy(other_host);
174 my_id->destroy(my_id);
175 other_id->destroy(other_id);
176 this->stroke_logger->log(this->stroke_logger, ERROR, "invalid subnet: %s", msg->add_conn.me.subnet);
177 return;
178 }
179
180 other_subnet = host_create(AF_INET, *msg->add_conn.other.subnet ? msg->add_conn.other.subnet : msg->add_conn.other.address, 500);
181 if (other_subnet == NULL)
182 {
183 my_host->destroy(my_host);
184 other_host->destroy(other_host);
185 my_id->destroy(my_id);
186 other_id->destroy(other_id);
187 my_subnet->destroy(my_subnet);
188 this->stroke_logger->log(this->stroke_logger, ERROR, "invalid subnet: %s", msg->add_conn.me.subnet);
189 return;
190 }
191
192 my_ts = traffic_selector_create_from_subnet(my_subnet, *msg->add_conn.me.subnet ? msg->add_conn.me.subnet_mask : 32);
193 my_subnet->destroy(my_subnet);
194 other_ts = traffic_selector_create_from_subnet(other_subnet, *msg->add_conn.other.subnet ? msg->add_conn.other.subnet_mask : 32);
195 other_subnet->destroy(other_subnet);
196
197 if (charon->socket->is_listening_on(charon->socket, other_host))
198 {
199 this->stroke_logger->log(this->stroke_logger, CONTROL|LEVEL1, "left is other host, switching");
200
201 host_t *tmp_host = my_host;
202 identification_t *tmp_id = my_id;
203 traffic_selector_t *tmp_ts = my_ts;
204 char *tmp_cert = msg->add_conn.me.cert;
205
206 my_host = other_host;
207 other_host = tmp_host;
208 my_id = other_id;
209 other_id = tmp_id;
210 my_ts = other_ts;
211 other_ts = tmp_ts;
212 msg->add_conn.me.cert = msg->add_conn.other.cert;
213 msg->add_conn.other.cert = tmp_cert;
214 }
215 else if (charon->socket->is_listening_on(charon->socket, my_host))
216 {
217 this->stroke_logger->log(this->stroke_logger, CONTROL|LEVEL1, "left is own host, not switching");
218 }
219 else
220 {
221 this->stroke_logger->log(this->stroke_logger, ERROR, "left nor right host is our, aborting");
222
223 my_host->destroy(my_host);
224 other_host->destroy(other_host);
225 my_id->destroy(my_id);
226 other_id->destroy(other_id);
227 my_ts->destroy(my_ts);
228 other_ts->destroy(other_ts);
229 return;
230 }
231
232 if (msg->add_conn.me.cert)
233 {
234 char file[128];
235 snprintf(file, sizeof(file), "%s%s", CERTIFICATE_DIR, msg->add_conn.me.cert);
236 cert = x509_create_from_file(file);
237 if (cert)
238 {
239 my_id->destroy(my_id);
240 my_id = cert->get_subject(cert);
241 my_id = my_id->clone(my_id);
242 cert->destroy(cert);
243 this->stroke_logger->log(this->stroke_logger, CONTROL|LEVEL1,
244 "defined a valid certificate, using its ID \"%s\"",
245 my_id->get_string(my_id));
246 }
247 }
248 if (msg->add_conn.other.cert)
249 {
250 char file[128];
251 snprintf(file, sizeof(file), "%s%s", CERTIFICATE_DIR, msg->add_conn.other.cert);
252 cert = x509_create_from_file(file);
253 if (cert)
254 {
255 other_id->destroy(other_id);
256 other_id = cert->get_subject(cert);
257 other_id = other_id->clone(other_id);
258 cert->destroy(cert);
259 this->stroke_logger->log(this->stroke_logger, CONTROL|LEVEL1,
260 "defined a valid certificate, using its ID \"%s\"",
261 other_id->get_string(other_id));
262 }
263 }
264
265 connection = connection_create(msg->add_conn.name,
266 my_host, other_host,
267 my_id->clone(my_id), other_id->clone(other_id),
268 RSA_DIGITAL_SIGNATURE);
269 proposal = proposal_create(1);
270 proposal->add_algorithm(proposal, PROTO_IKE, ENCRYPTION_ALGORITHM, ENCR_AES_CBC, 16);
271 proposal->add_algorithm(proposal, PROTO_IKE, INTEGRITY_ALGORITHM, AUTH_HMAC_SHA1_96, 0);
272 proposal->add_algorithm(proposal, PROTO_IKE, INTEGRITY_ALGORITHM, AUTH_HMAC_MD5_96, 0);
273 proposal->add_algorithm(proposal, PROTO_IKE, PSEUDO_RANDOM_FUNCTION, PRF_HMAC_SHA1, 0);
274 proposal->add_algorithm(proposal, PROTO_IKE, PSEUDO_RANDOM_FUNCTION, PRF_HMAC_MD5, 0);
275 proposal->add_algorithm(proposal, PROTO_IKE, DIFFIE_HELLMAN_GROUP, MODP_2048_BIT, 0);
276 proposal->add_algorithm(proposal, PROTO_IKE, DIFFIE_HELLMAN_GROUP, MODP_1536_BIT, 0);
277 proposal->add_algorithm(proposal, PROTO_IKE, DIFFIE_HELLMAN_GROUP, MODP_1024_BIT, 0);
278 proposal->add_algorithm(proposal, PROTO_IKE, DIFFIE_HELLMAN_GROUP, MODP_4096_BIT, 0);
279 proposal->add_algorithm(proposal, PROTO_IKE, DIFFIE_HELLMAN_GROUP, MODP_8192_BIT, 0);
280 connection->add_proposal(connection, proposal);
281 /* add to global connection list */
282 charon->connections->add_connection(charon->connections, connection);
283
284 policy = policy_create(my_id, other_id);
285 proposal = proposal_create(1);
286 proposal->add_algorithm(proposal, PROTO_ESP, ENCRYPTION_ALGORITHM, ENCR_AES_CBC, 16);
287 proposal->add_algorithm(proposal, PROTO_ESP, INTEGRITY_ALGORITHM, AUTH_HMAC_SHA1_96, 0);
288 proposal->add_algorithm(proposal, PROTO_ESP, INTEGRITY_ALGORITHM, AUTH_HMAC_MD5_96, 0);
289 policy->add_proposal(policy, proposal);
290 policy->add_my_traffic_selector(policy, my_ts);
291 policy->add_other_traffic_selector(policy, other_ts);
292 /* add to global policy list */
293 charon->policies->add_policy(charon->policies, policy);
294
295 this->stroke_logger->log(this->stroke_logger, CONTROL|LEVEL1, "connection \"%s\" added", msg->add_conn.name);
296 }
297
298 /**
299 * initiate a connection by name
300 */
301 static void stroke_initiate(private_stroke_t *this, stroke_msg_t *msg)
302 {
303 initiate_ike_sa_job_t *job;
304 connection_t *connection;
305
306 pop_string(msg, &(msg->initiate.name));
307 this->logger->log(this->logger, CONTROL, "received stroke: initiate \"%s\"", msg->initiate.name);
308 connection = charon->connections->get_connection_by_name(charon->connections, msg->initiate.name);
309 if (connection == NULL)
310 {
311 this->stroke_logger->log(this->stroke_logger, ERROR, "could not find a connection named \"%s\"", msg->initiate.name);
312 }
313 else
314 {
315 job = initiate_ike_sa_job_create(connection->clone(connection));
316 charon->job_queue->add(charon->job_queue, (job_t*)job);
317 }
318 }
319
320 /**
321 * terminate a connection by name
322 */
323 static void stroke_terminate(private_stroke_t *this, stroke_msg_t *msg)
324 {
325 connection_t *connection;
326 ike_sa_t *ike_sa;
327 host_t *my_host, *other_host;
328 status_t status;
329
330 pop_string(msg, &(msg->terminate.name));
331 this->logger->log(this->logger, CONTROL, "received stroke: terminate \"%s\"", msg->terminate.name);
332 connection = charon->connections->get_connection_by_name(charon->connections, msg->terminate.name);
333
334 if (connection)
335 {
336 my_host = connection->get_my_host(connection);
337 other_host = connection->get_other_host(connection);
338
339 /* TODO: Do this directly by name now */
340 /* TODO: terminate any instance of the name */
341 status = charon->ike_sa_manager->checkout_by_hosts(charon->ike_sa_manager,
342 my_host, other_host, &ike_sa);
343
344 if (status == SUCCESS)
345 {
346 this->stroke_logger->log(this->stroke_logger, CONTROL, "deleting IKE SA between %s - %s",
347 my_host->get_address(my_host), other_host->get_address(other_host));
348
349 charon->ike_sa_manager->checkin_and_delete(charon->ike_sa_manager, ike_sa);
350 }
351 else
352 {
353 this->stroke_logger->log(this->stroke_logger, ERROR, "no active connection found between %s - %s",
354 my_host->get_address(my_host), other_host->get_address(other_host));
355 }
356 }
357 else
358 {
359 this->stroke_logger->log(this->stroke_logger, ERROR, "could not find a connection named \"%s\"", msg->terminate.name);
360 }
361
362 }
363
364 /**
365 * show status of (established) connections
366 */
367 static void stroke_status(private_stroke_t *this, stroke_msg_t *msg)
368 {
369 if (msg->status.name)
370 {
371 pop_string(msg, &(msg->status.name));
372 }
373 charon->ike_sa_manager->log_status(charon->ike_sa_manager, this->stroke_logger, msg->status.name);
374 }
375
376 logger_context_t get_context(char *context)
377 {
378 if (strcasecmp(context, "ALL") == 0) return ALL_LOGGERS;
379 else if (strcasecmp(context, "PARSR") == 0) return PARSER;
380 else if (strcasecmp(context, "GNRAT") == 0) return GENERATOR;
381 else if (strcasecmp(context, "IKESA") == 0) return IKE_SA;
382 else if (strcasecmp(context, "SAMGR") == 0) return IKE_SA_MANAGER;
383 else if (strcasecmp(context, "CHDSA") == 0) return CHILD_SA;
384 else if (strcasecmp(context, "MESSG") == 0) return MESSAGE;
385 else if (strcasecmp(context, "TPOOL") == 0) return THREAD_POOL;
386 else if (strcasecmp(context, "WORKR") == 0) return WORKER;
387 else if (strcasecmp(context, "SCHED") == 0) return SCHEDULER;
388 else if (strcasecmp(context, "SENDR") == 0) return SENDER;
389 else if (strcasecmp(context, "RECVR") == 0) return RECEIVER;
390 else if (strcasecmp(context, "SOCKT") == 0) return SOCKET;
391 else if (strcasecmp(context, "TESTR") == 0) return TESTER;
392 else if (strcasecmp(context, "DAEMN") == 0) return DAEMON;
393 else if (strcasecmp(context, "CONFG") == 0) return CONFIG;
394 else if (strcasecmp(context, "ENCPL") == 0) return ENCRYPTION_PAYLOAD;
395 else if (strcasecmp(context, "PAYLD") == 0) return PAYLOAD;
396 else return -2;
397 }
398
399 /**
400 * set the type of logged messages in a context
401 */
402 static void stroke_logtype(private_stroke_t *this, stroke_msg_t *msg)
403 {
404 pop_string(msg, &(msg->logtype.context));
405 pop_string(msg, &(msg->logtype.type));
406
407 this->logger->log(this->logger, CONTROL, "received stroke: logtype for %s", msg->logtype.context);
408
409 log_level_t level;
410 logger_context_t context = get_context(msg->logtype.context);
411 if (context == -2)
412 {
413 this->stroke_logger->log(this->stroke_logger, ERROR, "invalid context (%s)!", msg->logtype.context);
414 return;
415 }
416
417 if (strcasecmp(msg->logtype.type, "CONTROL") == 0) level = CONTROL;
418 else if (strcasecmp(msg->logtype.type, "ERROR") == 0) level = ERROR;
419 else if (strcasecmp(msg->logtype.type, "AUDIT") == 0) level = AUDIT;
420 else if (strcasecmp(msg->logtype.type, "RAW") == 0) level = RAW;
421 else if (strcasecmp(msg->logtype.type, "PRIVATE") == 0) level = PRIVATE;
422 else
423 {
424 this->stroke_logger->log(this->stroke_logger, ERROR, "invalid type (%s)!", msg->logtype.type);
425 return;
426 }
427
428 if (msg->logtype.enable)
429 {
430 logger_manager->enable_log_level(logger_manager, context, level);
431 }
432 else
433 {
434 logger_manager->disable_log_level(logger_manager, context, level);
435 }
436 }
437
438 /**
439 * set the verbosity of a logger
440 */
441 static void stroke_loglevel(private_stroke_t *this, stroke_msg_t *msg)
442 {
443 pop_string(msg, &(msg->loglevel.context));
444
445 this->logger->log(this->logger, CONTROL, "received stroke: loglevel for %s", msg->loglevel.context);
446
447 log_level_t level;
448 logger_context_t context = get_context(msg->loglevel.context);
449
450 if (context == -2)
451 {
452 this->stroke_logger->log(this->stroke_logger, ERROR, "invalid context (%s)!", msg->loglevel.context);
453 return;
454 }
455
456 if (msg->loglevel.level == 0)
457 {
458 level = LEVEL0;
459 }
460 else if (msg->loglevel.level == 1)
461 {
462 level = LEVEL1;
463 }
464 else if (msg->loglevel.level == 2)
465 {
466 level = LEVEL2;
467 }
468 else if (msg->loglevel.level == 3)
469 {
470 level = LEVEL3;
471 }
472 else
473 {
474 this->stroke_logger->log(this->stroke_logger, ERROR, "invalid level (%d)!", msg->loglevel.level);
475 return;
476 }
477
478 logger_manager->enable_log_level(logger_manager, context, level);
479 }
480
481 /**
482 * Implementation of private_stroke_t.stroke_receive.
483 */
484 static void stroke_receive(private_stroke_t *this)
485 {
486 stroke_msg_t *msg;
487 u_int16_t msg_length;
488 struct sockaddr_un strokeaddr;
489 int strokeaddrlen = sizeof(strokeaddr);
490 ssize_t bytes_read;
491 int strokefd;
492 FILE *strokefile;
493 int oldstate;
494
495 /* disable cancellation by default */
496 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
497
498 while (1)
499 {
500 /* wait for connections, but allow thread to terminate */
501 pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldstate);
502 strokefd = accept(this->socket, (struct sockaddr *)&strokeaddr, &strokeaddrlen);
503 pthread_setcancelstate(oldstate, NULL);
504
505 if (strokefd < 0)
506 {
507 this->logger->log(this->logger, ERROR, "accepting stroke connection failed: %s", strerror(errno));
508 continue;
509 }
510
511 /* peek the length */
512 bytes_read = recv(strokefd, &msg_length, sizeof(msg_length), MSG_PEEK);
513 if (bytes_read != sizeof(msg_length))
514 {
515 this->logger->log(this->logger, ERROR, "reading lenght of stroke message failed");
516 close(strokefd);
517 continue;
518 }
519
520 /* read message */
521 msg = malloc(msg_length);
522 bytes_read = recv(strokefd, msg, msg_length, 0);
523 if (bytes_read != msg_length)
524 {
525 this->logger->log(this->logger, ERROR, "reading stroke message failed: %s");
526 close(strokefd);
527 continue;
528 }
529
530 strokefile = fdopen(dup(strokefd), "w");
531 if (strokefile == NULL)
532 {
533 this->logger->log(this->logger, ERROR, "opening stroke output channel failed:", strerror(errno));
534 close(strokefd);
535 free(msg);
536 continue;
537 }
538
539 this->stroke_logger = logger_create("-", CONTROL|ERROR, FALSE, strokefile);
540
541 this->logger->log_bytes(this->logger, RAW, "stroke message", (void*)msg, msg_length);
542
543 switch (msg->type)
544 {
545 case STR_INITIATE:
546 {
547 stroke_initiate(this, msg);
548 break;
549 }
550 case STR_TERMINATE:
551 {
552 stroke_terminate(this, msg);
553 break;
554 }
555 case STR_STATUS:
556 {
557 stroke_status(this, msg);
558 break;
559 }
560 case STR_STATUS_ALL:
561 {
562 this->stroke_logger->enable_level(this->stroke_logger, LEVEL1);
563 stroke_status(this, msg);
564 break;
565 }
566 case STR_ADD_CONN:
567 {
568 stroke_add_conn(this, msg);
569 break;
570 }
571 case STR_LOGTYPE:
572 {
573 stroke_logtype(this, msg);
574 break;
575 }
576 case STR_LOGLEVEL:
577 {
578 stroke_loglevel(this, msg);
579 break;
580 }
581 default:
582 this->logger->log(this->logger, ERROR, "received invalid stroke");
583 }
584 this->stroke_logger->destroy(this->stroke_logger);
585 fclose(strokefile);
586 close(strokefd);
587 free(msg);
588 }
589 }
590
591 /**
592 * Implementation of stroke_t.destroy.
593 */
594 static void destroy(private_stroke_t *this)
595 {
596
597 pthread_cancel(this->assigned_thread);
598 pthread_join(this->assigned_thread, NULL);
599
600 close(this->socket);
601 unlink(socket_addr.sun_path);
602 free(this);
603 }
604
605
606 /*
607 * Described in header-file
608 */
609 stroke_t *stroke_create()
610 {
611 private_stroke_t *this = malloc_thing(private_stroke_t);
612 mode_t old;
613
614 /* public functions */
615 this->public.destroy = (void (*)(stroke_t*))destroy;
616
617 /* private functions */
618 this->stroke_receive = stroke_receive;
619
620 this->logger = logger_manager->get_logger(logger_manager, CONFIG);
621
622 /* set up unix socket */
623 this->socket = socket(AF_UNIX, SOCK_STREAM, 0);
624 if (this->socket == -1)
625 {
626 this->logger->log(this->logger, ERROR, "could not create whack socket");
627 free(this);
628 return NULL;
629 }
630
631 old = umask(~S_IRWXU);
632 if (bind(this->socket, (struct sockaddr *)&socket_addr, sizeof(socket_addr)) < 0)
633 {
634 this->logger->log(this->logger, ERROR, "could not bind stroke socket: %s", strerror(errno));
635 close(this->socket);
636 free(this);
637 return NULL;
638 }
639 umask(old);
640
641 if (listen(this->socket, 0) < 0)
642 {
643 this->logger->log(this->logger, ERROR, "could not listen on stroke socket: %s", strerror(errno));
644 close(this->socket);
645 unlink(socket_addr.sun_path);
646 free(this);
647 return NULL;
648 }
649
650 /* start a thread reading from the socket */
651 if (pthread_create(&(this->assigned_thread), NULL, (void*(*)(void*))this->stroke_receive, this) != 0)
652 {
653 this->logger->log(this->logger, ERROR, "Could not spawn stroke thread");
654 close(this->socket);
655 unlink(socket_addr.sun_path);
656 free(this);
657 return NULL;
658 }
659
660 return (&this->public);
661 }