diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f532d9..13ecbb7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,7 +42,13 @@ Expected: September 2022 ### New features * RESTCONF call home according to RFC 8071 - * Experimental, work-in-progress + * clixon-restconf.yang extended with callhome inspired by ietf-restconf-server.yang + * See e.g., draft-ietf-netconf-restconf-client-server-26.txt + * The `` list has been extended with a `call-home` presence container including: + * reconnect-strategy/max-attempts + * connection-type: either persistent or periodic + * idle-timeout for periodic call-homes. + * An example util client is `clixon_restconf_callhome_client.c` used in test cases ### Corrected Bugs diff --git a/apps/restconf/restconf_lib.c b/apps/restconf/restconf_lib.c index ef021863..e561fe72 100644 --- a/apps/restconf/restconf_lib.c +++ b/apps/restconf/restconf_lib.c @@ -863,157 +863,3 @@ restconf_socket_init(const char *netns0, return retval; } -/*! Extract socket info from backend config - * @param[in] h Clicon handle - * @param[in] xs socket config - * @param[in] nsc Namespace context - * @param[out] description - * @param[out] namespace - * @param[out] address Address as string, eg "0.0.0.0", "::" - * @param[out] addrtype One of inet:ipv4-address or inet:ipv6-address - * @param[out] port TCP Port - * @param[out] ssl SSL enabled? - * @param[out] callhome Callhome - * if callhome: - * @param[out] periodic persistent:0, periodic:1 - * @param[out] period in s. (if periodic) - * @param[out] attempts Number of max reconnect attempts - */ -int -restconf_socket_extract(clicon_handle h, - cxobj *xs, - cvec *nsc, - char **description, - char **namespace, - char **address, - char **addrtype, - uint16_t *port, - uint16_t *ssl, - int *callhome, - int *periodic, - uint32_t *period, - uint8_t *attempts) -{ - int retval = -1; - cxobj *x; - char *str = NULL; - char *reason = NULL; - int ret; - char *body; - cg_var *cv = NULL; - yang_stmt *y; - yang_stmt *ysub = NULL; - - if ((x = xpath_first(xs, nsc, "namespace")) == NULL){ - clicon_err(OE_XML, EINVAL, "Mandatory namespace not given"); - goto done; - } - *namespace = xml_body(x); - if ((x = xpath_first(xs, nsc, "description")) != NULL){ - *description = xml_body(x); - } - if ((x = xpath_first(xs, nsc, "address")) == NULL){ - clicon_err(OE_XML, EINVAL, "Mandatory address not given"); - goto done; - } - /* address is a union type and needs a special investigation to see which type (ipv4 or ipv6) - * the address is - */ - body = xml_body(x); - y = xml_spec(x); - if ((cv = cv_dup(yang_cv_get(y))) == NULL){ - clicon_err(OE_UNIX, errno, "cv_dup"); - goto done; - } - if ((ret = cv_parse1(body, cv, &reason)) < 0){ - clicon_err(OE_XML, errno, "cv_parse1"); - goto done; - } - if (ret == 0){ - clicon_err(OE_XML, EFAULT, "%s", reason); - goto done; - } - if ((ret = ys_cv_validate(h, cv, y, &ysub, &reason)) < 0) - goto done; - if (ret == 0){ - clicon_err(OE_XML, EFAULT, "Validation os address: %s", reason); - goto done; - } - if (ysub == NULL){ - clicon_err(OE_XML, EFAULT, "No address union type"); - goto done; - } - *address = body; - /* This is YANG type name of ip-address: - * typedef ip-address { - * type union { - * type inet:ipv4-address; <--- - * type inet:ipv6-address; <--- - * } - */ - *addrtype = yang_argument_get(ysub); - if ((x = xpath_first(xs, nsc, "port")) != NULL && - (str = xml_body(x)) != NULL){ - if ((ret = parse_uint16(str, port, &reason)) < 0){ - clicon_err(OE_XML, errno, "parse_uint16"); - goto done; - } - if (ret == 0){ - clicon_err(OE_XML, EINVAL, "Unrecognized value of port: %s", str); - goto done; - } - } - if ((x = xpath_first(xs, nsc, "ssl")) != NULL && - (str = xml_body(x)) != NULL){ - /* XXX use parse_bool but it is legacy static */ - if (strcmp(str, "false") == 0) - *ssl = 0; - else if (strcmp(str, "true") == 0) - *ssl = 1; - else { - clicon_err(OE_XML, EINVAL, "Unrecognized value of ssl: %s", str); - goto done; - } - } - if (xpath_first(xs, nsc, "call-home") != NULL){ - *callhome = 1; - if (xpath_first(xs, nsc, "call-home/connection-type/persistent") != NULL){ - *periodic = 0; - } - else if (xpath_first(xs, nsc, "call-home/connection-type/periodic") != NULL){ - *periodic = 1; - if ((x = xpath_first(xs, nsc, "call-home/connection-type/periodic/period")) != NULL && - (str = xml_body(x)) != NULL){ - if ((ret = parse_uint32(str, period, &reason)) < 0){ - clicon_err(OE_XML, errno, "parse_uint16"); - goto done; - } - if (ret == 0){ - clicon_err(OE_XML, EINVAL, "Unrecognized value of period: %s", str); - goto done; - } - } - } - if ((x = xpath_first(xs, nsc, "call-home/reconnect-strategy/max-attempts")) != NULL && - (str = xml_body(x)) != NULL){ - if ((ret = parse_uint8(str, attempts, &reason)) < 0){ - clicon_err(OE_XML, errno, "parse_uint8"); - goto done; - } - if (ret == 0){ - clicon_err(OE_XML, EINVAL, "Unrecognized value of max-attempts: %s", str); - goto done; - } - } - } - else - *callhome = 0; - retval = 0; - done: - if (cv) - cv_free(cv); - if (reason) - free(reason); - return retval; -} - diff --git a/apps/restconf/restconf_lib.h b/apps/restconf/restconf_lib.h index 9b20aec7..e02d9329 100644 --- a/apps/restconf/restconf_lib.h +++ b/apps/restconf/restconf_lib.h @@ -97,7 +97,6 @@ int restconf_drop_privileges(clicon_handle h); int restconf_authentication_cb(clicon_handle h, void *req, int pretty, restconf_media media_out); int restconf_config_init(clicon_handle h, cxobj *xrestconf); int restconf_socket_init(const char *netns0, const char *addrstr, const char *addrtype, uint16_t port, int backlog, int flags, int *ss); -int restconf_socket_extract(clicon_handle h, cxobj *xs, cvec *nsc, char **description, char **namespace, char **address, char **addrtype, uint16_t *port, uint16_t *ssl, int *callhome, int *periodic, uint32_t *period, uint8_t *attempts); #endif /* _RESTCONF_LIB_H_ */ diff --git a/apps/restconf/restconf_main_native.c b/apps/restconf/restconf_main_native.c index ef117d2d..8638017b 100644 --- a/apps/restconf/restconf_main_native.c +++ b/apps/restconf/restconf_main_native.c @@ -171,7 +171,6 @@ static int session_id_context = 1; - /*! Set restconf native handle * @param[in] h Clicon handle * @param[in] rh Restconf native handle (malloced pointer) @@ -604,7 +603,9 @@ restconf_native_terminate(clicon_handle h) if ((rh = restconf_native_handle_get(h)) != NULL){ while ((rsock = rh->rh_sockets) != NULL){ if (rsock->rs_callhome){ - clixon_event_unreg_timeout(restconf_callhome_cb, rsock); + restconf_callhome_timer_unreg(rsock); + if (rsock->rs_periodic) + restconf_idle_timer_unreg(rsock); } else if (rsock->rs_ss != -1){ clixon_event_unreg_fd(rsock->rs_ss, restconf_accept_client); @@ -710,26 +711,16 @@ openssl_init_socket(clicon_handle h, cvec *nsc) { int retval = -1; - char *description = NULL; char *netns = NULL; char *address = NULL; char *addrtype = NULL; - int callhome = 0; - uint16_t ssl = 0; uint16_t port = 0; int ss = -1; restconf_native_handle *rh = NULL; restconf_socket *rsock = NULL; /* openssl per socket struct */ - int periodic = 0; - uint32_t period = 0; - uint8_t max_attempts = 0; struct timeval now; clicon_debug(1, "%s", __FUNCTION__); - /* Extract socket parameters from single socket config: ns, addr, port, ssl */ - if (restconf_socket_extract(h, xs, nsc, &description, &netns, &address, &addrtype, &port, &ssl, &callhome, &periodic, &period, &max_attempts) < 0) - goto done; - /* * Create per-socket openssl handle * See restconf_native_terminate for freeing @@ -740,20 +731,13 @@ openssl_init_socket(clicon_handle h, } memset(rsock, 0, sizeof *rsock); rsock->rs_h = h; - rsock->rs_ssl = ssl; /* true/false */ - rsock->rs_callhome = callhome; - rsock->rs_periodic = periodic; - rsock->rs_period = period; - rsock->rs_max_attempts = max_attempts; gettimeofday(&now, NULL); rsock->rs_start = now.tv_sec; - if (description && - (rsock->rs_description = strdup(description)) == NULL){ - clicon_err(OE_UNIX, errno, "strdup"); + /* Extract socket parameters from single socket config: ns, addr, port, ssl */ + if (restconf_socket_extract(h, xs, nsc, rsock, &netns, &address, &addrtype, &port) < 0) goto done; - } - if (callhome){ - if (!ssl){ + if (rsock->rs_callhome){ + if (!rsock->rs_ssl){ clicon_err(OE_SSL, EINVAL, "Restconf callhome requires SSL"); goto done; } @@ -786,7 +770,7 @@ openssl_init_socket(clicon_handle h, rsock->rs_port = port; INSQ(rsock, rh->rh_sockets); - if (callhome){ + if (rsock->rs_callhome){ rsock->rs_ss = -1; /* Not applicable from callhome */ if (restconf_callhome_timer(rsock, 0) < 0) goto done; diff --git a/apps/restconf/restconf_native.c b/apps/restconf/restconf_native.c index 9a22d334..165dd790 100644 --- a/apps/restconf/restconf_native.c +++ b/apps/restconf/restconf_native.c @@ -309,8 +309,8 @@ restconf_connection_sanity(clicon_handle h, clicon_err(OE_UNIX, errno, "cbuf_new"); goto done; } - cprintf(cberr, "HTTP cert verification failed: %s", - X509_verify_cert_error_string(code)); + cprintf(cberr, "HTTP cert verification failed: %s[%ld]", + X509_verify_cert_error_string(code), code); if (netconf_invalid_value_xml(&xerr, "protocol", cbuf_get(cberr)) < 0) goto done; if ((media_str = restconf_param_get(h, "HTTP_ACCEPT")) == NULL){ @@ -975,16 +975,20 @@ restconf_connection_close(clicon_handle h, { int retval = -1; - clicon_debug(1, "%s", __FUNCTION__); + clicon_debug(1, "%s %s", __FUNCTION__, rsock->rs_description); if (close(s) < 0){ clicon_err(OE_UNIX, errno, "close"); goto done; } clixon_event_unreg_fd(s, restconf_connection); /* re-set timer */ - if (rsock->rs_callhome && - restconf_callhome_timer(rsock, 1) < 0) - goto done; + if (rsock->rs_callhome){ + if (rsock->rs_periodic && + restconf_idle_timer_unreg(rsock) < 0) + goto done; + if (restconf_callhome_timer(rsock, 1) < 0) + goto done; + } retval = 0; done: clicon_debug(1, "%s %d", __FUNCTION__, retval); @@ -1328,6 +1332,81 @@ restconf_ssl_accept_client(clicon_handle h, return retval; } /* restconf_ssl_accept_client */ +/*! idle timeout timer callback + */ +static int +restconf_idle_cb(int fd, + void *arg) +{ + int retval = -1; + restconf_socket *rsock; + + if ((rsock = (restconf_socket *)arg) == NULL){ + clicon_err(OE_YANG, EINVAL, "rsock is NULL"); + goto done; + } + clicon_debug(1, "%s %s", __FUNCTION__, rsock->rs_description); + /* sanity */ + if (rsock->rs_callhome && rsock->rs_periodic && rsock->rs_idle_timeout && rsock->rs_ss > 0){ + if (restconf_connection_close(rsock->rs_h, rsock->rs_ss, rsock) < 0) + goto done; + } + retval = 0; + done: + return retval; +} + +int +restconf_idle_timer_unreg(restconf_socket *rsock) +{ + return clixon_event_unreg_timeout(restconf_idle_cb, rsock); +} + +/*! Set callhome periodic idle-timeout + * 1) If callhome and periodic, set timer for t0+idle-timeout(ti) + * 2) Timestamp any data passing on the socket(td) + * 3) At timeout (ti) check if ti = td+idle-timeout (for first timeout same as t0=td), + * if so close socket + * 4) Else, reset timer to ti = td + idle-timeout. Goto (2) + * socket. + * XXX: now just timeout dont keep track of data (td) + * @see restconf_idle_timer_unreg + */ +int +restconf_idle_timer(restconf_socket *rsock) +{ + int retval = -1; + struct timeval now; + struct timeval t; + struct timeval t1 = {0, 0}; + cbuf *cb = NULL; + + if (rsock == NULL || !rsock->rs_callhome || !rsock->rs_periodic || rsock->rs_idle_timeout==0){ + clicon_err(OE_YANG, EINVAL, "rsock is NULL or not periodic callhome"); + goto done; + } + clicon_debug(1, "%s %s register", __FUNCTION__, rsock->rs_description); + if ((cb = cbuf_new()) == NULL){ + clicon_err(OE_UNIX, errno, "cbuf_new"); + goto done; + } + cprintf(cb, "restconf idle timer %s", rsock->rs_description); + gettimeofday(&now, NULL); + t1.tv_sec = rsock->rs_idle_timeout; + timeradd(&now, &t1, &t); + clicon_debug(1, "%s now:%lu timeout:%lu.%lu", __FUNCTION__, now.tv_sec, t.tv_sec, t.tv_usec); + if (clixon_event_reg_timeout(t, + restconf_idle_cb, + rsock, + cbuf_get(cb)) < 0) + goto done; + retval = 0; + done: + if (cb) + cbuf_free(cb); + return retval; +} + /*! Callhome timer callback * * @param[in] fd No-op @@ -1336,7 +1415,7 @@ restconf_ssl_accept_client(clicon_handle h, * the reason is that this function may continue with SSL accept handling whereas the wrapper always * returns directly. */ -int +static int restconf_callhome_cb(int fd, void *arg) { @@ -1348,14 +1427,12 @@ restconf_callhome_cb(int fd, size_t sa_len; int s; - if ((rsock = (restconf_socket *)arg) == NULL){ + rsock = (restconf_socket *)arg; + if (rsock == NULL || !rsock->rs_callhome){ clicon_err(OE_YANG, EINVAL, "rsock is NULL"); goto done; } - if (rsock->rs_description) - clicon_debug(1, "%s %s", __FUNCTION__, rsock->rs_description); - else - clicon_debug(1, "%s", __FUNCTION__); + clicon_debug(1, "%s %s", __FUNCTION__, rsock->rs_description); h = rsock->rs_h; /* Already computed in restconf_socket_init, could be saved in rsock? */ if (clixon_inet2sin(rsock->rs_addrtype, rsock->rs_addrstr, rsock->rs_port, sa, &sa_len) < 0) @@ -1379,6 +1456,9 @@ restconf_callhome_cb(int fd, rsock->rs_attempts = 0; if (restconf_ssl_accept_client(h, s, rsock) < 0) goto done; + if (rsock->rs_periodic && rsock->rs_idle_timeout && + restconf_idle_timer(rsock) < 0) + goto done; } clicon_debug(1, "%s connect done", __FUNCTION__); retval = 0; @@ -1386,16 +1466,23 @@ restconf_callhome_cb(int fd, return retval; } +int +restconf_callhome_timer_unreg(restconf_socket *rsock) +{ + return clixon_event_unreg_timeout(restconf_callhome_cb, rsock); +} + /*! Set callhome timer, which tries to connect to callhome client * * Implement callhome re-connect strategies in ietf-restconf-server.yang * NYI: start-with, anchor-time, idle-timeout * @param[in] rsock restconf_socket - * @param[in] new Start a new period (if periodic) + * @param[in] new if periodic: 1: Force a new period + * @see restconf_callhome_timer_unreg */ int restconf_callhome_timer(restconf_socket *rsock, - int new) + int status) { int retval = -1; struct timeval now; @@ -1403,15 +1490,16 @@ restconf_callhome_timer(restconf_socket *rsock, struct timeval t1 = {0, 0}; cbuf *cb = NULL; - if (rsock->rs_description) - clicon_debug(1, "%s %s", __FUNCTION__, rsock->rs_description); - else - clicon_debug(1, "%s", __FUNCTION__); + if (rsock == NULL || !rsock->rs_callhome){ + clicon_err(OE_YANG, EINVAL, "rsock is NULL or not callhome"); + goto done; + } + clicon_debug(1, "%s %s", __FUNCTION__, rsock->rs_description); if (!rsock->rs_callhome) goto ok; /* shouldnt happen */ gettimeofday(&now, NULL); if (rsock->rs_periodic){ - if (new || rsock->rs_attempts >= rsock->rs_max_attempts){ + if ((status == 1) || rsock->rs_attempts >= rsock->rs_max_attempts){ rsock->rs_period_nr++; rsock->rs_attempts = 0; t1.tv_sec = rsock->rs_start + rsock->rs_period_nr*rsock->rs_period; @@ -1432,10 +1520,7 @@ restconf_callhome_timer(restconf_socket *rsock, clicon_err(OE_UNIX, errno, "cbuf_new"); goto done; } - if (rsock->rs_description) - cprintf(cb, "restconf callhome timer %s", rsock->rs_description); - else - cprintf(cb, "restconf callhome timer"); + cprintf(cb, "restconf callhome timer %s", rsock->rs_description); if (rsock->rs_description) clicon_debug(1, "%s registering %s: %lu", __FUNCTION__, rsock->rs_description, t.tv_sec); else @@ -1453,3 +1538,159 @@ restconf_callhome_timer(restconf_socket *rsock, cbuf_free(cb); return retval; } + +/*! Extract socket info from backend config + * @param[in] h Clicon handle + * @param[in] xs socket config + * @param[in] nsc Namespace context + * @param[out] rsock restconf socket data, filled in with many fields + * @param[out] namespace + * @param[out] address Address as string, eg "0.0.0.0", "::" + * @param[out] addrtype One of inet:ipv4-address or inet:ipv6-address + * @param[out] port TCP Port + */ +int +restconf_socket_extract(clicon_handle h, + cxobj *xs, + cvec *nsc, + restconf_socket *rsock, + char **namespace, + char **address, + char **addrtype, + uint16_t *port) +{ + int retval = -1; + cxobj *x; + char *str = NULL; + char *reason = NULL; + int ret; + char *body; + cg_var *cv = NULL; + yang_stmt *y; + yang_stmt *ysub = NULL; + + if ((x = xpath_first(xs, nsc, "namespace")) == NULL){ + clicon_err(OE_XML, EINVAL, "Mandatory namespace not given"); + goto done; + } + *namespace = xml_body(x); + if ((x = xpath_first(xs, nsc, "description")) != NULL){ + if ((rsock->rs_description = strdup(xml_body(x))) == NULL){ + clicon_err(OE_UNIX, errno, "strdup"); + goto done; + } + } + if ((x = xpath_first(xs, nsc, "address")) == NULL){ + clicon_err(OE_XML, EINVAL, "Mandatory address not given"); + goto done; + } + /* address is a union type and needs a special investigation to see which type (ipv4 or ipv6) + * the address is + */ + body = xml_body(x); + y = xml_spec(x); + if ((cv = cv_dup(yang_cv_get(y))) == NULL){ + clicon_err(OE_UNIX, errno, "cv_dup"); + goto done; + } + if ((ret = cv_parse1(body, cv, &reason)) < 0){ + clicon_err(OE_XML, errno, "cv_parse1"); + goto done; + } + if (ret == 0){ + clicon_err(OE_XML, EFAULT, "%s", reason); + goto done; + } + if ((ret = ys_cv_validate(h, cv, y, &ysub, &reason)) < 0) + goto done; + if (ret == 0){ + clicon_err(OE_XML, EFAULT, "Validation os address: %s", reason); + goto done; + } + if (ysub == NULL){ + clicon_err(OE_XML, EFAULT, "No address union type"); + goto done; + } + *address = body; + /* This is YANG type name of ip-address: + * typedef ip-address { + * type union { + * type inet:ipv4-address; <--- + * type inet:ipv6-address; <--- + * } + */ + *addrtype = yang_argument_get(ysub); + if ((x = xpath_first(xs, nsc, "port")) != NULL && + (str = xml_body(x)) != NULL){ + if ((ret = parse_uint16(str, port, &reason)) < 0){ + clicon_err(OE_XML, errno, "parse_uint16"); + goto done; + } + if (ret == 0){ + clicon_err(OE_XML, EINVAL, "Unrecognized value of port: %s", str); + goto done; + } + } + if ((x = xpath_first(xs, nsc, "ssl")) != NULL && + (str = xml_body(x)) != NULL){ + /* XXX use parse_bool but it is legacy static */ + if (strcmp(str, "false") == 0) + rsock->rs_ssl = 0; + else if (strcmp(str, "true") == 0) + rsock->rs_ssl = 1; + else { + clicon_err(OE_XML, EINVAL, "Unrecognized value of ssl: %s", str); + goto done; + } + } + if (xpath_first(xs, nsc, "call-home") != NULL){ + rsock->rs_callhome = 1; + if (xpath_first(xs, nsc, "call-home/connection-type/persistent") != NULL){ + rsock->rs_periodic = 0; + } + else if (xpath_first(xs, nsc, "call-home/connection-type/periodic") != NULL){ + rsock->rs_periodic = 1; + if ((x = xpath_first(xs, nsc, "call-home/connection-type/periodic/period")) != NULL && + (str = xml_body(x)) != NULL){ + if ((ret = parse_uint32(str, &rsock->rs_period, &reason)) < 0){ + clicon_err(OE_XML, errno, "parse_uint16"); + goto done; + } + if (ret == 0){ + clicon_err(OE_XML, EINVAL, "Unrecognized value of period: %s", str); + goto done; + } + } + if ((x = xpath_first(xs, nsc, "call-home/connection-type/periodic/idle-timeout")) != NULL && + (str = xml_body(x)) != NULL){ + if ((ret = parse_uint16(str, &rsock->rs_idle_timeout, &reason)) < 0){ + clicon_err(OE_XML, errno, "parse_uint16"); + goto done; + } + if (ret == 0){ + clicon_err(OE_XML, EINVAL, "Unrecognized value of idle-timeout: %s", str); + goto done; + } + } + } + if ((x = xpath_first(xs, nsc, "call-home/reconnect-strategy/max-attempts")) != NULL && + (str = xml_body(x)) != NULL){ + if ((ret = parse_uint8(str, &rsock->rs_max_attempts, &reason)) < 0){ + clicon_err(OE_XML, errno, "parse_uint8"); + goto done; + } + if (ret == 0){ + clicon_err(OE_XML, EINVAL, "Unrecognized value of max-attempts: %s", str); + goto done; + } + } + } + retval = 0; + done: + if (cv) + cv_free(cv); + if (reason) + free(reason); + return retval; +} + diff --git a/apps/restconf/restconf_native.h b/apps/restconf/restconf_native.h index 34a6fa01..3f7bcde5 100644 --- a/apps/restconf/restconf_native.h +++ b/apps/restconf/restconf_native.h @@ -107,7 +107,7 @@ typedef struct { int rs_periodic; /* 0: persistent, 1: periodic (if callhome) */ uint32_t rs_period; /* Period in s (if callhome & periodic) */ uint8_t rs_max_attempts; /* max connect attempts (if callhome) */ - + uint16_t rs_idle_timeout; /* Max underlying TCP session remains idle (if callhome and periodic) */ uint64_t rs_start; /* First period start, next is start+periods*period */ uint64_t rs_period_nr; /* Dynamic succeeding or timed out periods. Set in restconf_callhome_timer*/ @@ -165,9 +165,12 @@ restconf_native_handle *restconf_native_handle_get(clicon_handle h); int restconf_connection(int s, void *arg); int restconf_connection_close(clicon_handle h, int s, restconf_socket *rsock); int restconf_ssl_accept_client(clicon_handle h, int s, restconf_socket *rsock); -int restconf_callhome_cb(int fd, void *arg); - -int restconf_callhome_timer(restconf_socket *rsock, int new); +int restconf_idle_timer_unreg(restconf_socket *rsock); +int restconf_idle_timer(restconf_socket *rsock); +int restconf_callhome_timer_unreg(restconf_socket *rsock); +int restconf_callhome_timer(restconf_socket *rsock, int status); +int restconf_socket_extract(clicon_handle h, cxobj *xs, cvec *nsc, restconf_socket *rsock, + char **namespace, char **address, char **addrtype, uint16_t *port); #endif /* _RESTCONF_NATIVE_H_ */ diff --git a/lib/src/clixon_event.c b/lib/src/clixon_event.c index 36dfcdc4..746e3287 100644 --- a/lib/src/clixon_event.c +++ b/lib/src/clixon_event.c @@ -284,8 +284,10 @@ clixon_event_reg_timeout(struct timeval t, * Note: deregister when exactly function and function arguments match, not time. So you * cannot have same function and argument callback on different timeouts. This is a little * different from clixon_event_unreg_fd. - * @param[in] fn Function to call at time t - * @param[in] arg Argument to function fn + * @param[in] fn Function to call at time t + * @param[in] arg Argument to function fn + * @retval 0 OK, timeout unregistered + * @retval -1 OK, but timeout not found * @see clixon_event_reg_timeout * @see clixon_event_unreg_fd */ diff --git a/test/test_restconf_callhome.sh b/test/test_restconf_callhome.sh index f069cf08..0897a885 100755 --- a/test/test_restconf_callhome.sh +++ b/test/test_restconf_callhome.sh @@ -78,7 +78,7 @@ cat < $cfg - 3 + 1
127.0.0.1
@@ -92,10 +92,11 @@ cat < $cfg 10 + 5 - 3 + 1
127.0.0.1
@@ -290,13 +291,13 @@ fi new "wait restconf" wait_restconf + new "restconf Add init data" expectpart "$(curl $CURLOPTS --key $certdir/andy.key --cert $certdir/andy.crt -X POST -H "Accept: application/yang-data+json" -H "Content-Type: application/yang-data+json" -d '{"clixon-example:table":{"parameter":{"name":"x","value":"foo"}}}' $RCPROTO://127.0.0.1/restconf/data)" 0 "HTTP/$HVER 201" -# tests for (one) well-formed HTTP status and three replies and that the time is reasonable t0=$(date +"%s") new "Send GET via callhome persistence client port 4336" -expectpart "$(${clixon_restconf_callhome_client} -p 4336 -D $DBG -f $frequest -a 127.0.0.1 -c $srvcert -k $srvkey -C $cacert -n 3)" 0 "HTTP/$HVERCH 200" "OK 1" "OK 2" "OK 3" $expectreply --not-- "OK 4" +expectpart "$(${clixon_restconf_callhome_client} -p 4336 -D $DBG -f $frequest -a 127.0.0.1 -c $srvcert -k $srvkey -C $cacert -n 3)" 0 "HTTP/$HVERCH 200" "OK 1" "Close 1 local" "OK 2" "Close 2 local" "OK 3" "Close 3 local" $expectreply --not-- "OK 4" "Close 4" t1=$(date +"%s") let t=t1-t0 @@ -307,18 +308,18 @@ fi t0=$(date +"%s") new "Send GET via callhome client periodic port 8336" -expectpart "$(${clixon_restconf_callhome_client} -p 8336 -D $DBG -f $frequest -a 127.0.0.1 -c $srvcert -k $srvkey -C $cacert -n 3)" 0 "HTTP/$HVERCH 200" "OK 1" "OK 2" "OK 3" $expectreply --not-- "OK "4 +expectpart "$(${clixon_restconf_callhome_client} -t 30 -p 8336 -D $DBG -f $frequest -a 127.0.0.1 -c $srvcert -k $srvkey -C $cacert -n 2)" 0 "HTTP/$HVERCH 200" "OK 1" "Close 1" "OK 2" "Close 2" $expectreply --not-- "OK 3" "Close 3" t1=$(date +"%s") let t=t1-t0 -new "Check periodic interval ($t) is larger than 20 and less than 31" -if [ $t -lt 20 -o $t -ge 31 ]; then - err1 "timer in interval [20-31] but is: $t" +new "Check periodic interval ($t) is in interval [10-21]" +if [ $t -lt 10 -o $t -ge 21 ]; then + err1 "timer in interval [10-21] but is: $t" fi t0=$(date +"%s") new "Send GET via callhome persistence again" -expectpart "$(${clixon_restconf_callhome_client} -p 4336 -D $DBG -f $frequest -a 127.0.0.1 -c $srvcert -k $srvkey -C $cacert -n 3)" 0 "OK 1" "OK 2" "OK 3" --not-- "OK 4" +expectpart "$(${clixon_restconf_callhome_client} -p 4336 -D $DBG -f $frequest -a 127.0.0.1 -c $srvcert -k $srvkey -C $cacert -n 3)" 0 "HTTP/$HVERCH 200" "OK 1" "Close 1 local" "OK 2" "Close 2 local" "OK 3" "Close 3 local" $expectreply --not-- "OK 4" "Close 4" t1=$(date +"%s") let t=t1-t0 @@ -327,7 +328,17 @@ if [ $t -lt 2 -o $t -ge 4 ]; then err1 "timer in interval [2,4] but is: $t" fi -# Kill old +t0=$(date +"%s") +new "Send GET and try idle-timeout, clients keeps socket open" +expectpart "$(${clixon_restconf_callhome_client} -o -t 30 -p 8336 -D $DBG -f $frequest -a 127.0.0.1 -c $srvcert -k $srvkey -C $cacert -n 1)" 0 "HTTP/$HVERCH 200" "OK 1" $expectreply "Close 1 remote" --not-- "OK 2" "Close 2" +t1=$(date +"%s") + +let t=t1-t0 +new "Check periodic interval ($t) is in interval [5-15]" +if [ $t -lt 5 -o $t -ge 15 ]; then + err1 "timer in interval [5-15] but is: $t" +fi + if [ $RC -ne 0 ]; then new "Kill restconf daemon" stop_restconf diff --git a/util/clixon_restconf_callhome_client.c b/util/clixon_restconf_callhome_client.c index 1f8f3288..a42ea0d2 100644 --- a/util/clixon_restconf_callhome_client.c +++ b/util/clixon_restconf_callhome_client.c @@ -68,7 +68,7 @@ /* clixon */ #include "clixon/clixon.h" -#define UTIL_TLS_OPTS "hD:f:e:F:a:p:c:C:k:n:" +#define UTIL_TLS_OPTS "hD:f:F:a:p:c:C:k:n:ot:" #define RESTCONF_CH_TLS 4336 @@ -86,7 +86,13 @@ typedef struct { } tls_session_data; /* Expected connects */ -static int _connects = 1; +static int _connects = 1; + +/* Keep socket open, dont close immediately after reply */ +static int _keep_open = 0; + +/* Timeout in seconds after each accept, if fired just exit */ +static int _timeout_s = 60; /*! Create and bind stream socket * @param[in] sa Socketaddress @@ -186,6 +192,9 @@ read_data_file(FILE *fe, } /*! Client data socket, receive reply from server + * Print info on stdout + * If keep_open = 0, then close socket directly after 1st reply (client close) + * If keep_open = 1, then keep socket open (server close) */ static int tls_server_reply_cb(int s, @@ -199,10 +208,12 @@ tls_server_reply_cb(int s, char *expbuf = NULL; struct timeval tn; struct timeval td; - static int seq = 1; + static int seq = 0; static struct timeval t0 = {0,}; clicon_debug(1, "%s", __FUNCTION__); + if (seq == 0) + gettimeofday(&t0, NULL); ssl = sd->sd_ssl; /* get reply & decrypt */ if ((n = SSL_read(ssl, buf, sizeof(buf))) < 0){ @@ -210,24 +221,42 @@ tls_server_reply_cb(int s, goto done; } if (n == 0){ - clicon_debug(1, "%s closed", __FUNCTION__); - goto done; + SSL_shutdown(ssl); + SSL_free(ssl); + clixon_event_unreg_fd(s, tls_server_reply_cb); + fprintf(stdout, "Close %d remote %lu\n", seq, td.tv_sec); + close(s); + free(sd); + if (_connects == 0) + ; + else if (_connects == 1) + clixon_exit_set(1); /* XXX more elaborate logic: 1) continue request, 2) close and accept new */ + else + _connects--; + goto ok; } + seq++; buf[n] = 0; - if (seq == 1) - gettimeofday(&t0, NULL); gettimeofday(&tn, NULL); timersub(&tn, &t0, &td); - fprintf(stdout, "OK %d %lu\n%s\n", seq++, td.tv_sec, buf); - SSL_shutdown(ssl); - SSL_free(ssl); - clixon_event_unreg_fd(s, tls_server_reply_cb); - close(s); - free(sd); - if (_connects == 1) - clixon_exit_set(1); /* XXX more elaborate logic: 1) continue request, 2) close and accept new */ - else - _connects--; + fprintf(stdout, "OK %d %lu\n%s\n", seq, td.tv_sec, buf); + if (!_keep_open){ + SSL_shutdown(ssl); + SSL_free(ssl); + clixon_event_unreg_fd(s, tls_server_reply_cb); + fprintf(stdout, "Close %d local %lu\n", seq, td.tv_sec); + close(s); + if (_connects == 0) + ; + else if (_connects == 1){ + if (!_keep_open) + clixon_exit_set(1); /* XXX more elaborate logic: 1) continue request, 2) close and accept new */ + } + else + _connects--; + free(sd); + } + ok: retval = 0; done: if (expbuf) @@ -330,6 +359,42 @@ tls_write_file(FILE *fp, return retval; } +static int +tls_timeout_cb(int fd, + void *arg) +{ + tls_accept_handle *ta = (tls_accept_handle *)arg; + + fprintf(stderr, "Timeout on socket:%d\n", ta->ta_ss); + exit(200); +} + +/*! Timeout in seconds after each accept, if fired just exit + */ +static int +tls_client_timeout(void *arg) +{ + int retval = -1; + struct timeval now; + struct timeval t; + struct timeval t1 = {0, 0}; + + /* Unregister existing timeout */ + clixon_event_unreg_timeout(tls_timeout_cb, arg); + /* Set timeout */ + gettimeofday(&now, NULL); + t1.tv_sec = _timeout_s; + timeradd(&now, &t1, &t); + if (clixon_event_reg_timeout(t, + tls_timeout_cb, + arg, + "tls client timeout") < 0) + goto done; + retval = 0; + done: + return retval; +} + /*! Callhome-server accept socket */ static int @@ -367,6 +432,10 @@ tls_server_accept_cb(int ss, /* register callback for reply */ if (clixon_event_reg_fd(s, tls_server_reply_cb, sd, "tls server reply") < 0) goto done; + /* Unregister old + register new timeout */ + if (tls_client_timeout(ta) < 0) + goto done; + retval = 0; done: return retval; @@ -439,14 +508,15 @@ usage(char *argv0) "\t-h \t\tHelp\n" "\t-D \tDebug\n" "\t-f \tHTTP input file (overrides stdin)\n" - "\t-e \tFile including HTTP output (otherwise echo to stdout)\n" "\t-F ipv4|ipv6 \tSocket address family(ipv4 default)\n" "\t-a \tIP address (eg 1.2.3.4) - mandatory\n" "\t-p \tPort (default %d)\n" "\t-c \tcert\n" "\t-C \tcacert\n" "\t-k \tkey\n" - "\t-n \tExpected incoming connections, 0 means no limit. Default: 1\n" + "\t-n \tQuit after this many incoming connections, 0 means no limit. Default: 1\n" + "\t-o \tKeep open after receiving first reply. Otherwise close directly after receiving 1st reply\n" + "\t-t \tTimeout in seconds after each accept, if fired just exit. Default: 60s\n" , argv0, RESTCONF_CH_TLS); @@ -525,6 +595,14 @@ main(int argc, usage(argv[0]); _connects = atoi(optarg); break; + case 'o': /* keep open, do not close after first reply */ + _keep_open = 1; + break; + case 't': /* timeout */ + if (optarg == NULL || *optarg == '-') + usage(argv[0]); + _timeout_s = atoi(optarg); + break; default: usage(argv[0]); break; @@ -569,6 +647,8 @@ main(int argc, ta->ta_f = fp; if (clixon_event_reg_fd(ss, tls_server_accept_cb, ta, "tls server accept") < 0) goto done; + if (tls_client_timeout(ta) < 0) + goto done; if (clixon_event_loop(h) < 0) goto done; retval = 0; diff --git a/yang/clixon/clixon-restconf@2022-08-01.yang b/yang/clixon/clixon-restconf@2022-08-01.yang index ddcc20d6..a7a37702 100644 --- a/yang/clixon/clixon-restconf@2022-08-01.yang +++ b/yang/clixon/clixon-restconf@2022-08-01.yang @@ -25,34 +25,8 @@ module clixon-restconf { 3. Related to (2), options that should not be settable in a datastore should be in clixon-config - ***** BEGIN LICENSE BLOCK ***** - Copyright (C) 2020-2022 Olof Hagsand and Rubicon Communications, LLC(Netgate) - - This file is part of CLIXON - - Licensed under the Apache License, Version 2.0 (the \"License\"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an \"AS IS\" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - Alternatively, the contents of this file may be used under the terms of - the GNU General Public License Version 3 or later (the \"GPL\"), - in which case the provisions of the GPL are applicable instead - of those above. If you wish to allow use of your version of this file only - under the terms of the GPL, and not to allow others to - use your version of this file under the terms of Apache License version 2, - indicate your decision by deleting the provisions above and replace them with - the notice and other provisions required by the GPL. If you do not delete - the provisions above, a recipient may use your version of this file under - the terms of any one of the Apache License version 2 or the GPL. - - ***** END LICENSE BLOCK *****"; - + Some of this spec if in-lined from ietf-restconf-server@2022-05-24.yang + "; revision 2022-08-01 { description "Added socket/call-home container @@ -303,11 +277,23 @@ module clixon-restconf { "Indicates periodic connects"; leaf period { type uint32; /* XXX: note uit16 in std */ - units "seconds"; /* XXX: note minutes in standard */ - default "60"; + units "seconds"; /* XXX: note minutes in draft */ + default "3600"; /* XXX: same: 60min in draft */ description "Duration of time between periodic connections."; } + leaf idle-timeout { + type uint16; + units "seconds"; + default "120"; // two minutes + description + "Specifies the maximum number of seconds that + the underlying TCP session may remain idle. + A TCP session will be dropped if it is idle + for an interval longer than this number of + seconds. If set to zero, then the server + will never drop a session because it is idle."; + } } } }