610 lines
14 KiB
C
610 lines
14 KiB
C
/* $OpenBSD: control.c,v 1.117 2024/04/22 09:36:04 claudio Exp $ */
|
|
|
|
/*
|
|
* Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <errno.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
|
|
#include "bgpd.h"
|
|
#include "session.h"
|
|
#include "log.h"
|
|
|
|
TAILQ_HEAD(ctl_conns, ctl_conn) ctl_conns = TAILQ_HEAD_INITIALIZER(ctl_conns);
|
|
|
|
#define CONTROL_BACKLOG 5
|
|
|
|
struct ctl_conn *control_connbyfd(int);
|
|
struct ctl_conn *control_connbypid(pid_t);
|
|
int control_close(struct ctl_conn *);
|
|
void control_result(struct ctl_conn *, u_int);
|
|
ssize_t imsg_read_nofd(struct imsgbuf *);
|
|
|
|
int
|
|
control_check(char *path)
|
|
{
|
|
struct sockaddr_un sa_un;
|
|
int fd;
|
|
|
|
memset(&sa_un, 0, sizeof(sa_un));
|
|
sa_un.sun_family = AF_UNIX;
|
|
strlcpy(sa_un.sun_path, path, sizeof(sa_un.sun_path));
|
|
|
|
if ((fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)) == -1) {
|
|
log_warn("%s: socket", __func__);
|
|
return (-1);
|
|
}
|
|
|
|
if (connect(fd, (struct sockaddr *)&sa_un, sizeof(sa_un)) == 0) {
|
|
log_warnx("control socket %s already in use", path);
|
|
close(fd);
|
|
return (-1);
|
|
}
|
|
|
|
close(fd);
|
|
|
|
return (0);
|
|
}
|
|
|
|
int
|
|
control_init(int restricted, char *path)
|
|
{
|
|
struct sockaddr_un sa_un;
|
|
int fd;
|
|
mode_t old_umask, mode;
|
|
|
|
if ((fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK,
|
|
0)) == -1) {
|
|
log_warn("control_init: socket");
|
|
return (-1);
|
|
}
|
|
|
|
memset(&sa_un, 0, sizeof(sa_un));
|
|
sa_un.sun_family = AF_UNIX;
|
|
if (strlcpy(sa_un.sun_path, path, sizeof(sa_un.sun_path)) >=
|
|
sizeof(sa_un.sun_path)) {
|
|
log_warn("control_init: socket name too long");
|
|
close(fd);
|
|
return (-1);
|
|
}
|
|
|
|
if (unlink(path) == -1)
|
|
if (errno != ENOENT) {
|
|
log_warn("control_init: unlink %s", path);
|
|
close(fd);
|
|
return (-1);
|
|
}
|
|
|
|
if (restricted) {
|
|
old_umask = umask(S_IXUSR|S_IXGRP|S_IXOTH);
|
|
mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH;
|
|
} else {
|
|
old_umask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH);
|
|
mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP;
|
|
}
|
|
|
|
if (bind(fd, (struct sockaddr *)&sa_un, sizeof(sa_un)) == -1) {
|
|
log_warn("control_init: bind: %s", path);
|
|
close(fd);
|
|
umask(old_umask);
|
|
return (-1);
|
|
}
|
|
|
|
umask(old_umask);
|
|
|
|
if (chmod(path, mode) == -1) {
|
|
log_warn("control_init: chmod: %s", path);
|
|
close(fd);
|
|
unlink(path);
|
|
return (-1);
|
|
}
|
|
|
|
return (fd);
|
|
}
|
|
|
|
int
|
|
control_listen(int fd)
|
|
{
|
|
if (fd != -1 && listen(fd, CONTROL_BACKLOG) == -1) {
|
|
log_warn("control_listen: listen");
|
|
return (-1);
|
|
}
|
|
|
|
return (0);
|
|
}
|
|
|
|
void
|
|
control_shutdown(int fd)
|
|
{
|
|
close(fd);
|
|
}
|
|
|
|
size_t
|
|
control_fill_pfds(struct pollfd *pfd, size_t size)
|
|
{
|
|
struct ctl_conn *ctl_conn;
|
|
size_t i = 0;
|
|
|
|
TAILQ_FOREACH(ctl_conn, &ctl_conns, entry) {
|
|
pfd[i].fd = ctl_conn->imsgbuf.fd;
|
|
pfd[i].events = POLLIN;
|
|
if (ctl_conn->imsgbuf.w.queued > 0)
|
|
pfd[i].events |= POLLOUT;
|
|
i++;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
unsigned int
|
|
control_accept(int listenfd, int restricted)
|
|
{
|
|
int connfd;
|
|
socklen_t len;
|
|
struct sockaddr_un sa_un;
|
|
struct ctl_conn *ctl_conn;
|
|
|
|
len = sizeof(sa_un);
|
|
if ((connfd = accept4(listenfd,
|
|
(struct sockaddr *)&sa_un, &len,
|
|
SOCK_NONBLOCK | SOCK_CLOEXEC)) == -1) {
|
|
if (errno == ENFILE || errno == EMFILE) {
|
|
pauseaccept = getmonotime();
|
|
return (0);
|
|
} else if (errno != EWOULDBLOCK && errno != EINTR &&
|
|
errno != ECONNABORTED)
|
|
log_warn("control_accept: accept");
|
|
return (0);
|
|
}
|
|
|
|
if ((ctl_conn = calloc(1, sizeof(struct ctl_conn))) == NULL) {
|
|
log_warn("control_accept");
|
|
close(connfd);
|
|
return (0);
|
|
}
|
|
|
|
imsg_init(&ctl_conn->imsgbuf, connfd);
|
|
ctl_conn->restricted = restricted;
|
|
|
|
TAILQ_INSERT_TAIL(&ctl_conns, ctl_conn, entry);
|
|
|
|
return (1);
|
|
}
|
|
|
|
struct ctl_conn *
|
|
control_connbyfd(int fd)
|
|
{
|
|
struct ctl_conn *c;
|
|
|
|
TAILQ_FOREACH(c, &ctl_conns, entry) {
|
|
if (c->imsgbuf.fd == fd)
|
|
break;
|
|
}
|
|
|
|
return (c);
|
|
}
|
|
|
|
struct ctl_conn *
|
|
control_connbypid(pid_t pid)
|
|
{
|
|
struct ctl_conn *c;
|
|
|
|
TAILQ_FOREACH(c, &ctl_conns, entry) {
|
|
if (c->imsgbuf.pid == pid)
|
|
break;
|
|
}
|
|
|
|
return (c);
|
|
}
|
|
|
|
int
|
|
control_close(struct ctl_conn *c)
|
|
{
|
|
if (c->terminate && c->imsgbuf.pid)
|
|
imsg_ctl_rde_msg(IMSG_CTL_TERMINATE, 0, c->imsgbuf.pid);
|
|
|
|
msgbuf_clear(&c->imsgbuf.w);
|
|
TAILQ_REMOVE(&ctl_conns, c, entry);
|
|
|
|
close(c->imsgbuf.fd);
|
|
free(c);
|
|
pauseaccept = 0;
|
|
return (1);
|
|
}
|
|
|
|
int
|
|
control_dispatch_msg(struct pollfd *pfd, struct peer_head *peers)
|
|
{
|
|
struct imsg imsg;
|
|
struct ctl_neighbor neighbor;
|
|
struct ctl_show_rib_request ribreq;
|
|
struct ctl_conn *c;
|
|
struct peer *p;
|
|
ssize_t n;
|
|
uint32_t type;
|
|
pid_t pid;
|
|
int verbose, matched;
|
|
|
|
if ((c = control_connbyfd(pfd->fd)) == NULL) {
|
|
log_warn("control_dispatch_msg: fd %d: not found", pfd->fd);
|
|
return (0);
|
|
}
|
|
|
|
if (pfd->revents & POLLOUT) {
|
|
if (msgbuf_write(&c->imsgbuf.w) <= 0 && errno != EAGAIN)
|
|
return control_close(c);
|
|
if (c->throttled && c->imsgbuf.w.queued < CTL_MSG_LOW_MARK) {
|
|
if (imsg_ctl_rde_msg(IMSG_XON, 0, c->imsgbuf.pid) != -1)
|
|
c->throttled = 0;
|
|
}
|
|
}
|
|
|
|
if (!(pfd->revents & POLLIN))
|
|
return (0);
|
|
|
|
if (((n = imsg_read_nofd(&c->imsgbuf)) == -1 && errno != EAGAIN) ||
|
|
n == 0)
|
|
return control_close(c);
|
|
|
|
for (;;) {
|
|
if ((n = imsg_get(&c->imsgbuf, &imsg)) == -1)
|
|
return control_close(c);
|
|
|
|
if (n == 0)
|
|
break;
|
|
|
|
type = imsg_get_type(&imsg);
|
|
pid = imsg_get_pid(&imsg);
|
|
if (c->restricted) {
|
|
switch (type) {
|
|
case IMSG_CTL_SHOW_NEIGHBOR:
|
|
case IMSG_CTL_SHOW_NEXTHOP:
|
|
case IMSG_CTL_SHOW_INTERFACE:
|
|
case IMSG_CTL_SHOW_RIB_MEM:
|
|
case IMSG_CTL_SHOW_TERSE:
|
|
case IMSG_CTL_SHOW_TIMER:
|
|
case IMSG_CTL_SHOW_NETWORK:
|
|
case IMSG_CTL_SHOW_FLOWSPEC:
|
|
case IMSG_CTL_SHOW_RIB:
|
|
case IMSG_CTL_SHOW_RIB_PREFIX:
|
|
case IMSG_CTL_SHOW_SET:
|
|
case IMSG_CTL_SHOW_RTR:
|
|
break;
|
|
default:
|
|
/* clear imsg type to prevent processing */
|
|
type = IMSG_NONE;
|
|
control_result(c, CTL_RES_DENIED);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* TODO: this is wrong and shoud work the other way around.
|
|
* The imsg.hdr.pid is from the remote end and should not
|
|
* be trusted.
|
|
*/
|
|
c->imsgbuf.pid = pid;
|
|
switch (type) {
|
|
case IMSG_NONE:
|
|
/* message was filtered out, nothing to do */
|
|
break;
|
|
case IMSG_CTL_FIB_COUPLE:
|
|
case IMSG_CTL_FIB_DECOUPLE:
|
|
imsg_ctl_parent(&imsg);
|
|
break;
|
|
case IMSG_CTL_SHOW_TERSE:
|
|
RB_FOREACH(p, peer_head, peers)
|
|
imsg_compose(&c->imsgbuf,
|
|
IMSG_CTL_SHOW_NEIGHBOR, 0, 0, -1,
|
|
p, sizeof(struct peer));
|
|
imsg_compose(&c->imsgbuf, IMSG_CTL_END, 0, 0, -1,
|
|
NULL, 0);
|
|
break;
|
|
case IMSG_CTL_SHOW_NEIGHBOR:
|
|
if (imsg_get_data(&imsg, &neighbor,
|
|
sizeof(neighbor)) == -1)
|
|
memset(&neighbor, 0, sizeof(neighbor));
|
|
|
|
matched = 0;
|
|
RB_FOREACH(p, peer_head, peers) {
|
|
if (!peer_matched(p, &neighbor))
|
|
continue;
|
|
|
|
matched = 1;
|
|
if (!neighbor.show_timers) {
|
|
imsg_ctl_rde_msg(type,
|
|
p->conf.id, pid);
|
|
} else {
|
|
u_int i;
|
|
time_t d;
|
|
struct ctl_timer ct;
|
|
|
|
imsg_compose(&c->imsgbuf,
|
|
IMSG_CTL_SHOW_NEIGHBOR,
|
|
0, 0, -1, p, sizeof(*p));
|
|
for (i = 1; i < Timer_Max; i++) {
|
|
if (!timer_running(&p->timers,
|
|
i, &d))
|
|
continue;
|
|
ct.type = i;
|
|
ct.val = d;
|
|
imsg_compose(&c->imsgbuf,
|
|
IMSG_CTL_SHOW_TIMER,
|
|
0, 0, -1, &ct, sizeof(ct));
|
|
}
|
|
}
|
|
}
|
|
if (!matched && RB_EMPTY(peers)) {
|
|
control_result(c, CTL_RES_NOSUCHPEER);
|
|
} else if (!neighbor.show_timers) {
|
|
imsg_ctl_rde_msg(IMSG_CTL_END, 0, pid);
|
|
} else {
|
|
imsg_compose(&c->imsgbuf, IMSG_CTL_END, 0, 0,
|
|
-1, NULL, 0);
|
|
}
|
|
break;
|
|
case IMSG_CTL_NEIGHBOR_UP:
|
|
case IMSG_CTL_NEIGHBOR_DOWN:
|
|
case IMSG_CTL_NEIGHBOR_CLEAR:
|
|
case IMSG_CTL_NEIGHBOR_RREFRESH:
|
|
case IMSG_CTL_NEIGHBOR_DESTROY:
|
|
if (imsg_get_data(&imsg, &neighbor,
|
|
sizeof(neighbor)) == -1) {
|
|
log_warnx("got IMSG_CTL_NEIGHBOR_ with "
|
|
"wrong length");
|
|
break;
|
|
}
|
|
|
|
matched = 0;
|
|
RB_FOREACH(p, peer_head, peers) {
|
|
if (!peer_matched(p, &neighbor))
|
|
continue;
|
|
|
|
matched = 1;
|
|
|
|
switch (type) {
|
|
case IMSG_CTL_NEIGHBOR_UP:
|
|
bgp_fsm(p, EVNT_START);
|
|
p->conf.down = 0;
|
|
p->conf.reason[0] = '\0';
|
|
p->IdleHoldTime =
|
|
INTERVAL_IDLE_HOLD_INITIAL;
|
|
p->errcnt = 0;
|
|
control_result(c, CTL_RES_OK);
|
|
break;
|
|
case IMSG_CTL_NEIGHBOR_DOWN:
|
|
neighbor.reason[
|
|
sizeof(neighbor.reason) - 1] = '\0';
|
|
p->conf.down = 1;
|
|
session_stop(p, ERR_CEASE_ADMIN_DOWN,
|
|
neighbor.reason);
|
|
control_result(c, CTL_RES_OK);
|
|
break;
|
|
case IMSG_CTL_NEIGHBOR_CLEAR:
|
|
neighbor.reason[
|
|
sizeof(neighbor.reason) - 1] = '\0';
|
|
p->IdleHoldTime =
|
|
INTERVAL_IDLE_HOLD_INITIAL;
|
|
p->errcnt = 0;
|
|
if (!p->conf.down) {
|
|
session_stop(p,
|
|
ERR_CEASE_ADMIN_RESET,
|
|
neighbor.reason);
|
|
timer_set(&p->timers,
|
|
Timer_IdleHold,
|
|
SESSION_CLEAR_DELAY);
|
|
} else {
|
|
session_stop(p,
|
|
ERR_CEASE_ADMIN_DOWN,
|
|
neighbor.reason);
|
|
}
|
|
control_result(c, CTL_RES_OK);
|
|
break;
|
|
case IMSG_CTL_NEIGHBOR_RREFRESH:
|
|
if (session_neighbor_rrefresh(p))
|
|
control_result(c,
|
|
CTL_RES_NOCAP);
|
|
else
|
|
control_result(c, CTL_RES_OK);
|
|
break;
|
|
case IMSG_CTL_NEIGHBOR_DESTROY:
|
|
if (!p->template)
|
|
control_result(c,
|
|
CTL_RES_BADPEER);
|
|
else if (p->state != STATE_IDLE)
|
|
control_result(c,
|
|
CTL_RES_BADSTATE);
|
|
else {
|
|
/*
|
|
* Mark as deleted, will be
|
|
* collected on next poll loop.
|
|
*/
|
|
p->reconf_action =
|
|
RECONF_DELETE;
|
|
control_result(c, CTL_RES_OK);
|
|
}
|
|
break;
|
|
default:
|
|
fatal("king bula wants more humppa");
|
|
}
|
|
}
|
|
if (!matched)
|
|
control_result(c, CTL_RES_NOSUCHPEER);
|
|
break;
|
|
case IMSG_CTL_RELOAD:
|
|
case IMSG_CTL_SHOW_INTERFACE:
|
|
case IMSG_CTL_SHOW_FIB_TABLES:
|
|
case IMSG_CTL_SHOW_RTR:
|
|
imsg_ctl_parent(&imsg);
|
|
break;
|
|
case IMSG_CTL_KROUTE:
|
|
case IMSG_CTL_KROUTE_ADDR:
|
|
case IMSG_CTL_SHOW_NEXTHOP:
|
|
imsg_ctl_parent(&imsg);
|
|
break;
|
|
case IMSG_CTL_SHOW_RIB:
|
|
case IMSG_CTL_SHOW_RIB_PREFIX:
|
|
if (imsg_get_data(&imsg, &ribreq, sizeof(ribreq)) ==
|
|
-1) {
|
|
log_warnx("got IMSG_CTL_SHOW_RIB with "
|
|
"wrong length");
|
|
break;
|
|
}
|
|
|
|
/* check if at least one neighbor exists */
|
|
RB_FOREACH(p, peer_head, peers)
|
|
if (peer_matched(p, &ribreq.neighbor))
|
|
break;
|
|
if (p == NULL && RB_EMPTY(peers)) {
|
|
control_result(c, CTL_RES_NOSUCHPEER);
|
|
break;
|
|
}
|
|
|
|
if (type == IMSG_CTL_SHOW_RIB_PREFIX &&
|
|
ribreq.prefix.aid == AID_UNSPEC) {
|
|
/* malformed request, must specify af */
|
|
control_result(c, CTL_RES_PARSE_ERROR);
|
|
break;
|
|
}
|
|
|
|
c->terminate = 1;
|
|
imsg_ctl_rde(&imsg);
|
|
break;
|
|
case IMSG_CTL_SHOW_NETWORK:
|
|
case IMSG_CTL_SHOW_FLOWSPEC:
|
|
c->terminate = 1;
|
|
/* FALLTHROUGH */
|
|
case IMSG_CTL_SHOW_RIB_MEM:
|
|
case IMSG_CTL_SHOW_SET:
|
|
imsg_ctl_rde(&imsg);
|
|
break;
|
|
case IMSG_NETWORK_ADD:
|
|
case IMSG_NETWORK_ASPATH:
|
|
case IMSG_NETWORK_ATTR:
|
|
case IMSG_NETWORK_REMOVE:
|
|
case IMSG_NETWORK_FLUSH:
|
|
case IMSG_NETWORK_DONE:
|
|
case IMSG_FLOWSPEC_ADD:
|
|
case IMSG_FLOWSPEC_REMOVE:
|
|
case IMSG_FLOWSPEC_DONE:
|
|
case IMSG_FLOWSPEC_FLUSH:
|
|
case IMSG_FILTER_SET:
|
|
imsg_ctl_rde(&imsg);
|
|
break;
|
|
case IMSG_CTL_LOG_VERBOSE:
|
|
if (imsg_get_data(&imsg, &verbose, sizeof(verbose)) ==
|
|
-1)
|
|
break;
|
|
|
|
/* forward to other processes */
|
|
imsg_ctl_parent(&imsg);
|
|
imsg_ctl_rde(&imsg);
|
|
log_setverbose(verbose);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
imsg_free(&imsg);
|
|
}
|
|
|
|
return (0);
|
|
}
|
|
|
|
int
|
|
control_imsg_relay(struct imsg *imsg, struct peer *p)
|
|
{
|
|
struct ctl_conn *c;
|
|
uint32_t type;
|
|
pid_t pid;
|
|
|
|
type = imsg_get_type(imsg);
|
|
pid = imsg_get_pid(imsg);
|
|
|
|
if ((c = control_connbypid(pid)) == NULL)
|
|
return (0);
|
|
|
|
/* special handling for peers since only the stats are sent from RDE */
|
|
if (type == IMSG_CTL_SHOW_NEIGHBOR) {
|
|
struct rde_peer_stats stats;
|
|
|
|
if (p == NULL) {
|
|
log_warnx("%s: no such peer: id=%u", __func__,
|
|
imsg_get_id(imsg));
|
|
return (0);
|
|
}
|
|
if (imsg_get_data(imsg, &stats, sizeof(stats)) == -1) {
|
|
log_warnx("%s: imsg_get_data", __func__);
|
|
return (0);
|
|
}
|
|
p->stats.prefix_cnt = stats.prefix_cnt;
|
|
p->stats.prefix_out_cnt = stats.prefix_out_cnt;
|
|
p->stats.prefix_rcvd_update = stats.prefix_rcvd_update;
|
|
p->stats.prefix_rcvd_withdraw = stats.prefix_rcvd_withdraw;
|
|
p->stats.prefix_rcvd_eor = stats.prefix_rcvd_eor;
|
|
p->stats.prefix_sent_update = stats.prefix_sent_update;
|
|
p->stats.prefix_sent_withdraw = stats.prefix_sent_withdraw;
|
|
p->stats.prefix_sent_eor = stats.prefix_sent_eor;
|
|
p->stats.pending_update = stats.pending_update;
|
|
p->stats.pending_withdraw = stats.pending_withdraw;
|
|
|
|
return imsg_compose(&c->imsgbuf, type, 0, pid, -1,
|
|
p, sizeof(*p));
|
|
}
|
|
|
|
/* if command finished no need to send exit message */
|
|
if (type == IMSG_CTL_END || type == IMSG_CTL_RESULT)
|
|
c->terminate = 0;
|
|
|
|
if (!c->throttled && c->imsgbuf.w.queued > CTL_MSG_HIGH_MARK) {
|
|
if (imsg_ctl_rde_msg(IMSG_XOFF, 0, pid) != -1)
|
|
c->throttled = 1;
|
|
}
|
|
|
|
return (imsg_forward(&c->imsgbuf, imsg));
|
|
}
|
|
|
|
void
|
|
control_result(struct ctl_conn *c, u_int code)
|
|
{
|
|
imsg_compose(&c->imsgbuf, IMSG_CTL_RESULT, 0, c->imsgbuf.pid, -1,
|
|
&code, sizeof(code));
|
|
}
|
|
|
|
/* This should go into libutil, from smtpd/mproc.c */
|
|
ssize_t
|
|
imsg_read_nofd(struct imsgbuf *imsgbuf)
|
|
{
|
|
ssize_t n;
|
|
char *buf;
|
|
size_t len;
|
|
|
|
buf = imsgbuf->r.buf + imsgbuf->r.wpos;
|
|
len = sizeof(imsgbuf->r.buf) - imsgbuf->r.wpos;
|
|
|
|
while ((n = recv(imsgbuf->fd, buf, len, 0)) == -1) {
|
|
if (errno != EINTR)
|
|
return (n);
|
|
}
|
|
|
|
imsgbuf->r.wpos += n;
|
|
return (n);
|
|
}
|