RESTCONF callhome, periodic and persistent connection-types, multiple clients

This commit is contained in:
Olof hagsand 2022-08-13 13:35:54 +02:00
parent 0676ee4e64
commit b748d68912
10 changed files with 765 additions and 913 deletions

View file

@ -867,23 +867,32 @@ restconf_socket_init(const char *netns0,
* @param[in] h Clicon handle * @param[in] h Clicon handle
* @param[in] xs socket config * @param[in] xs socket config
* @param[in] nsc Namespace context * @param[in] nsc Namespace context
* @param[out] description
* @param[out] namespace * @param[out] namespace
* @param[out] address Address as string, eg "0.0.0.0", "::" * @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] addrtype One of inet:ipv4-address or inet:ipv6-address
* @param[out] port TCP Port * @param[out] port TCP Port
* @param[out] ssl SSL enabled? * @param[out] ssl SSL enabled?
* @param[out] callhome Callhome 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 int
restconf_socket_extract(clicon_handle h, restconf_socket_extract(clicon_handle h,
cxobj *xs, cxobj *xs,
cvec *nsc, cvec *nsc,
char **description,
char **namespace, char **namespace,
char **address, char **address,
char **addrtype, char **addrtype,
uint16_t *port, uint16_t *port,
uint16_t *ssl, uint16_t *ssl,
int *callhome) int *callhome,
int *periodic,
uint32_t *period,
uint8_t *attempts)
{ {
int retval = -1; int retval = -1;
cxobj *x; cxobj *x;
@ -900,6 +909,9 @@ restconf_socket_extract(clicon_handle h,
goto done; goto done;
} }
*namespace = xml_body(x); *namespace = xml_body(x);
if ((x = xpath_first(xs, nsc, "description")) != NULL){
*description = xml_body(x);
}
if ((x = xpath_first(xs, nsc, "address")) == NULL){ if ((x = xpath_first(xs, nsc, "address")) == NULL){
clicon_err(OE_XML, EINVAL, "Mandatory address not given"); clicon_err(OE_XML, EINVAL, "Mandatory address not given");
goto done; goto done;
@ -963,8 +975,37 @@ restconf_socket_extract(clicon_handle h,
goto done; goto done;
} }
} }
if ((x = xpath_first(xs, nsc, "call-home")) != NULL) if (xpath_first(xs, nsc, "call-home") != NULL){
*callhome = 1; *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 else
*callhome = 0; *callhome = 0;
retval = 0; retval = 0;

View file

@ -97,7 +97,7 @@ int restconf_drop_privileges(clicon_handle h);
int restconf_authentication_cb(clicon_handle h, void *req, int pretty, restconf_media media_out); 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_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_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 **namespace, char **address, char **addrtype, uint16_t *port, uint16_t *ssl, int *callhome); 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_ */ #endif /* _RESTCONF_LIB_H_ */

View file

@ -474,7 +474,7 @@ Note that in this case SSL_ERROR_ZERO_RETURN does not necessarily indicate that
SSL_free(rc->rc_ssl); SSL_free(rc->rc_ssl);
rc->rc_ssl = NULL; rc->rc_ssl = NULL;
} }
if (restconf_connection_close(rc->rc_h, rc->rc_s) < 0) if (restconf_connection_close(rc->rc_h, rc->rc_s, rc->rc_socket) < 0)
goto done; goto done;
retval = 0; retval = 0;
done: done:
@ -548,342 +548,6 @@ restconf_checkcert_file(cxobj *xrestconf,
return retval; return retval;
} }
/*! Check ALPN result
* @proto[out] proto
* @retval 1 OK with proto set
* @retval 0 Fail, ALPN null or not recognized
* @retval -1 Error
*/
static int
ssl_alpn_check(clicon_handle h,
const unsigned char *alpn,
unsigned int alpnlen,
restconf_conn *rc,
restconf_http_proto *proto)
{
int retval = -1;
int ret;
cbuf *cberr = NULL;
clicon_debug(1, "%s", __FUNCTION__);
/* Alternatively, call restconf_str2proto but alpn is not a proper string */
if (alpn && alpnlen == 8 && memcmp("http/1.1", alpn, 8) == 0){
*proto = HTTP_11;
retval = 1; /* http/1.1 */
goto done;
}
#ifdef HAVE_LIBNGHTTP2
else if (alpn && alpnlen == 2 && memcmp("h2", alpn, 2) == 0){
*proto = HTTP_2;
retval = 1; /* http/2 */
goto done;
}
#endif
else {
if ((cberr = cbuf_new()) == NULL){
clicon_err(OE_UNIX, errno, "cbuf_new");
goto done;
}
if (alpn != NULL){
cprintf(cberr, "<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><error><error-type>protocol</error-type><error-tag>malformed-message</error-tag><error-message>ALPN: protocol not recognized: %s</error-message></error></errors>", alpn);
clicon_log(LOG_INFO, "%s Warning: %s", __FUNCTION__, cbuf_get(cberr));
if (native_send_badrequest(h, rc->rc_s, rc->rc_ssl,
"application/yang-data+xml",
cbuf_get(cberr)) < 0)
goto done;
}
else{
/* XXX Sending badrequest here gives a segv in SSL_shutdown() later or a SIGPIPE here */
clicon_log(LOG_INFO, "%s Warning: ALPN: No protocol selected, or no ALPN?", __FUNCTION__);
}
if (rc->rc_ssl){
/* nmap ssl-known-key SEGV at s->method->ssl_shutdown(s);
* OR OpenSSL error: : SSL_shutdown, err: SSL_ERROR_SYSCALL(5)
*/
if ((ret = SSL_shutdown(rc->rc_ssl)) < 0){
int e = SSL_get_error(rc->rc_ssl, ret);
if (e == SSL_ERROR_SYSCALL){
clicon_log(LOG_INFO, "%s Warning: SSL_shutdown SSL_ERROR_SYSCALL", __FUNCTION__);
/* Continue */
}
else {
clicon_err(OE_SSL, 0, "SSL_shutdown, err:%d", e);
goto done;
}
}
SSL_free(rc->rc_ssl);
}
restconf_conn_free(rc);
}
retval = 0; /* ALPN not OK */
done:
clicon_debug(1, "%s retval:%d", __FUNCTION__, retval);
if (cberr)
cbuf_free(cberr);
return retval;
} /* ssl_alpn_check */
/*! Accept new socket client. Note SSL not ip, this applies also to callhome
* @param[in] h Clixon handle
* @param[in] s Socket (unix or ip)
* @param[in] rsock Socket struct
* @see openssl_init_socket where this callback is registered
*/
static int
restconf_ssl_accept_client(clicon_handle h,
int s,
restconf_socket *rsock)
{
int retval = -1;
restconf_native_handle *rh = NULL;
restconf_conn *rc = NULL;
char *name = NULL;
int ret;
int e;
int er;
int readmore;
const unsigned char *alpn = NULL;
unsigned int alpnlen = 0;
restconf_http_proto proto = HTTP_11; /* Non-SSL negotiation NYI */
clicon_debug(1, "%s", __FUNCTION__);
#ifdef HAVE_LIBNGHTTP2
#ifndef HAVE_HTTP1
proto = HTTP_2; /* If nghttp2 only let default be 2.0 */
#endif
#endif
if ((rh = restconf_native_handle_get(h)) == NULL){
clicon_err(OE_XML, EFAULT, "No openssl handle");
goto done;
}
/*
* Register callbacks for actual data socket
*/
if ((rc = restconf_conn_new(h, s)) == NULL)
goto done;
clicon_debug(1, "%s s:%d", __FUNCTION__, rc->rc_s);
if (rsock->rs_ssl){
if ((rc->rc_ssl = SSL_new(rh->rh_ctx)) == NULL){
clicon_err(OE_SSL, 0, "SSL_new");
goto done;
}
clicon_debug(1, "%s SSL_new(%p)", __FUNCTION__, rc->rc_ssl);
/* CCL_CTX_set_verify already set, need not call SSL_set_verify again for this server
*/
/* X509_CHECK_FLAG_NO_WILDCARDS disables wildcard expansion */
SSL_set_hostflags(rc->rc_ssl, X509_CHECK_FLAG_NO_WILDCARDS);
#if 0
/* XXX This code is kept for the time being just for reference, it does not belong here.
* If you want to restrict client certs to a specific set.
* Otherwise this is done in restcon ca-auth callback and ultimately NACM
* SSL_set1_host() sets the expected DNS hostname to name
C = SE
L = Stockholm
O = Clixon
OU = clixon
CN = ca <---
emailAddress = olof@hagsand.se
*/
if (SSL_set1_host(rc->rc_ssl, "andy") != 1) { /* for peer cert */
clicon_err(OE_SSL, 0, "SSL_set1_host");
goto done;
}
if (SSL_add1_host(rc->rc_ssl, "olof") != 1) { /* for peer cert */
clicon_err(OE_SSL, 0, "SSL_set1_host");
goto done;
}
#endif
if (SSL_set_fd(rc->rc_ssl, rc->rc_s) != 1){
clicon_err(OE_SSL, 0, "SSL_set_fd");
goto done;
}
readmore = 1;
while (readmore){
readmore = 0;
/* 1: OK, -1 fatal, 0: TLS/SSL handshake was not successful
* Both error cases: Call SSL_get_error() with the return value ret
*/
if ((ret = SSL_accept(rc->rc_ssl)) != 1) {
clicon_debug(1, "%s SSL_accept() ret:%d errno:%d", __FUNCTION__, ret, er=errno);
e = SSL_get_error(rc->rc_ssl, ret);
switch (e){
case SSL_ERROR_SSL: /* 1 */
clicon_debug(1, "%s SSL_ERROR_SSL (non-ssl message on ssl socket)", __FUNCTION__);
#if 1
if (native_send_badrequest(h, rc->rc_s, NULL, "application/yang-data+xml",
"<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><error><error-type>protocol</error-type><error-tag>malformed-message</error-tag><error-message>The plain HTTP request was sent to HTTPS port</error-message></error></errors>") < 0)
goto done;
#endif
SSL_free(rc->rc_ssl);
rc->rc_ssl = NULL;
if (restconf_connection_close(h, rc->rc_s) < 0)
goto done;
restconf_conn_free(rc);
goto ok;
break;
case SSL_ERROR_SYSCALL: /* 5 */
/* Some non-recoverable, fatal I/O error occurred. The OpenSSL error queue
may contain more information on the error. For socket I/O on Unix systems,
consult errno for details. If this error occurs then no further I/O
operations should be performed on the connection and SSL_shutdown() must
not be called.*/
clicon_debug(1, "%s SSL_accept() SSL_ERROR_SYSCALL %d", __FUNCTION__, er);
if (restconf_close_ssl_socket(rc, 0) < 0)
goto done;
restconf_conn_free(rc);
rc = NULL;
goto ok;
break;
case SSL_ERROR_WANT_READ: /* 2 */
case SSL_ERROR_WANT_WRITE: /* 3 */
/* SSL_ERROR_WANT_READ is returned when the last operation was a read operation
* from a nonblocking BIO.
* That is, it can happen if restconf_socket_init() below is called
* with SOCK_NONBLOCK
*/
clicon_debug(1, "%s write SSL_ERROR_WANT_READ", __FUNCTION__);
usleep(10000);
readmore = 1;
break;
case SSL_ERROR_NONE: /* 0 */
case SSL_ERROR_ZERO_RETURN: /* 6 */
case SSL_ERROR_WANT_CONNECT: /* 7 */
case SSL_ERROR_WANT_ACCEPT: /* 8 */
case SSL_ERROR_WANT_X509_LOOKUP: /* 4 */
case SSL_ERROR_WANT_ASYNC: /* 8 */
case SSL_ERROR_WANT_ASYNC_JOB: /* 10 */
#ifdef SSL_ERROR_WANT_CLIENT_HELLO_CB
case SSL_ERROR_WANT_CLIENT_HELLO_CB: /* 11 */
#endif
default:
clicon_err(OE_SSL, 0, "SSL_accept:%d", e);
goto done;
break;
}
} /* SSL_accept */
} /* while(readmore) */
/* Sets data and len to point to the client's requested protocol for this connection. */
#ifndef OPENSSL_NO_NEXTPROTONEG
SSL_get0_next_proto_negotiated(rc->rc_ssl, &alpn, &alpnlen);
#endif /* !OPENSSL_NO_NEXTPROTONEG */
if (alpn == NULL) {
/* Returns a pointer to the selected protocol in data with length len. */
SSL_get0_alpn_selected(rc->rc_ssl, &alpn, &alpnlen);
}
if ((ret = ssl_alpn_check(h, alpn, alpnlen, rc, &proto)) < 0)
goto done;
if (ret == 0)
goto ok;
clicon_debug(1, "%s proto:%s", __FUNCTION__, restconf_proto2str(proto));
#if 0 /* Seems too early to fail here, instead let authentication callback deal with this */
/* For client-cert authentication, check if any certs are present,
* if not, send bad request
* Alt: set SSL_CTX_set_verify(ctx, SSL_VERIFY_FAIL_IF_NO_PEER_CERT)
* but then SSL_accept fails.
*/
if (restconf_auth_type_get(h) == CLIXON_AUTH_CLIENT_CERTIFICATE){
X509 *peercert;
if ((peercert = SSL_get_peer_certificate(rc->rc_ssl)) != NULL){
X509_free(peercert);
}
else { /* Get certificates (if available) */
if (proto != HTTP_2 &&
native_send_badrequest(h, rc->rc_s, rc->rc_ssl, "application/yang-data+xml",
"<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><error><error-type>protocol</error-type><error-tag>malformed-message</error-tag><error-message>Peer certificate required</error-message></error></errors>") < 0)
goto done;
restconf_conn_free(rc);
if (rc->rc_ssl){
if ((ret = SSL_shutdown(rc->rc_ssl)) < 0){
int e = SSL_get_error(rc->rc_ssl, ret);
clicon_err(OE_SSL, 0, "SSL_shutdown, err:%d", e);
goto done;
}
SSL_free(rc->rc_ssl);
rc->rc_ssl = NULL;
}
goto ok;
}
}
#endif
/* Get the actual peer, XXX this maybe could be done in ca-auth client-cert code ?
* Note this _only_ works if SSL_set1_host() was set previously,...
*/
if ((ret = SSL_get_verify_result(rc->rc_ssl)) == X509_V_OK) { /* for peer cert */
const char *peername = SSL_get0_peername(rc->rc_ssl);
if (peername != NULL) {
/* Name checks were in scope and matched the peername */
clicon_debug(1, "%s peername:%s", __FUNCTION__, peername);
}
}
#if 0
else{
clicon_log(LOG_NOTICE, "Cert error: %s", X509_verify_cert_error_string(ret));
/* Maybe should return already here, but to get proper return message need to
* continue to http/1 or http/2 handling
* @see restconf_connection_sanity
*/
}
#endif
#if 0 /* debug */
if (clicon_debug_get())
restconf_listcerts(rc->rc_ssl);
#endif
} /* if ssl */
rc->rc_proto = proto;
switch (rc->rc_proto){
#ifdef HAVE_HTTP1
case HTTP_10:
case HTTP_11:
/* Create a default stream for http/1 */
if (restconf_stream_data_new(rc, 0) == NULL)
goto done;
break;
#endif /* HAVE_HTTP1 */
#ifdef HAVE_LIBNGHTTP2
case HTTP_2:{
if (http2_session_init(rc) < 0){
restconf_close_ssl_socket(rc, 1);
goto done;
}
if (http2_send_server_connection(rc) < 0){
restconf_close_ssl_socket(rc, 1);
#ifdef NYI
if (ssl) {
SSL_shutdown(ssl);
}
bufferevent_free(session_data->bev);
nghttp2_session_del(session_data->session);
for (stream_data = session_data->root.next; stream_data;) {
http2_stream_data *next = stream_data->next;
delete_http2_stream_data(stream_data);
stream_data = next;
}
free(session_data->client_addr);
free(session_data);
#endif
goto done;
}
break;
}
#endif /* HAVE_LIBNGHTTP2 */
default:
break;
} /* switch proto */
if (clixon_event_reg_fd(rc->rc_s, restconf_connection, (void*)rc, "restconf client socket") < 0)
goto done;
ok:
retval = 0;
done:
clicon_debug(1, "%s retval %d", __FUNCTION__, retval);
if (name)
free(name);
return retval;
} /* restconf_ssl_accept_client */
/*! Accept new socket client /*! Accept new socket client
* @param[in] fd Socket (unix or ip) * @param[in] fd Socket (unix or ip)
* @param[in] arg typecast clicon_handle * @param[in] arg typecast clicon_handle
@ -939,11 +603,16 @@ restconf_native_terminate(clicon_handle h)
clicon_debug(1, "%s", __FUNCTION__); clicon_debug(1, "%s", __FUNCTION__);
if ((rh = restconf_native_handle_get(h)) != NULL){ if ((rh = restconf_native_handle_get(h)) != NULL){
while ((rsock = rh->rh_sockets) != NULL){ while ((rsock = rh->rh_sockets) != NULL){
if (rsock->rs_ss != -1){ if (rsock->rs_callhome){
clixon_event_unreg_timeout(restconf_callhome_cb, rsock);
}
else if (rsock->rs_ss != -1){
clixon_event_unreg_fd(rsock->rs_ss, restconf_accept_client); clixon_event_unreg_fd(rsock->rs_ss, restconf_accept_client);
close(rsock->rs_ss); close(rsock->rs_ss);
} }
DELQ(rsock, rh->rh_sockets, restconf_socket *); DELQ(rsock, rh->rh_sockets, restconf_socket *);
if (rsock->rs_description)
free(rsock->rs_description);
if (rsock->rs_addrstr) if (rsock->rs_addrstr)
free(rsock->rs_addrstr); free(rsock->rs_addrstr);
if (rsock->rs_addrtype) if (rsock->rs_addrtype)
@ -1028,62 +697,6 @@ restconf_clixon_backend(clicon_handle h,
goto done; goto done;
} }
/*! Periodically try to connect to callhome client
*
* @param[in] fd No-op
* @param[in] arg restconf_socket
*/
int
restconf_callhome_timer(int fdxxx,
void *arg)
{
int retval = -1;
clicon_handle h;
struct timeval now;
struct timeval t;
struct timeval t1 = {1, 0}; // XXX once every second
restconf_socket *rsock = NULL;
struct sockaddr_in6 sin6 = {0,}; // because its larger than sin and sa
struct sockaddr *sa = (struct sockaddr *)&sin6;
size_t sa_len;
int s;
clicon_debug(1, "%s", __FUNCTION__);
gettimeofday(&now, NULL);
if ((rsock = (restconf_socket *)arg) == NULL){
clicon_err(OE_YANG, EINVAL, "rsock is NULL");
goto done;
}
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)
goto done;
if ((s = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) {
clicon_err(OE_UNIX, errno, "socket");
goto done;
}
clicon_debug(1, "%s connect", __FUNCTION__);
if (connect(s, sa, sa_len) < 0){
clicon_debug(1, "%s connect:%d %s", __FUNCTION__, errno, strerror(errno));
close(s);
/* Fail: Initiate new timer */
timeradd(&now, &t1, &t);
if (clixon_event_reg_timeout(t,
restconf_callhome_timer, /* this function */
rsock,
"restconf callhome timer") < 0)
goto done;
}
else {
if (restconf_ssl_accept_client(h, s, rsock) < 0)
goto done;
}
clicon_debug(1, "%s connect done", __FUNCTION__);
retval = 0;
done:
return retval;
}
/*! Per-socket openssl inits /*! Per-socket openssl inits
* @param[in] h Clicon handle * @param[in] h Clicon handle
* @param[in] xs XML config of single restconf socket * @param[in] xs XML config of single restconf socket
@ -1097,6 +710,7 @@ openssl_init_socket(clicon_handle h,
cvec *nsc) cvec *nsc)
{ {
int retval = -1; int retval = -1;
char *description = NULL;
char *netns = NULL; char *netns = NULL;
char *address = NULL; char *address = NULL;
char *addrtype = NULL; char *addrtype = NULL;
@ -1106,10 +720,14 @@ openssl_init_socket(clicon_handle h,
int ss = -1; int ss = -1;
restconf_native_handle *rh = NULL; restconf_native_handle *rh = NULL;
restconf_socket *rsock = NULL; /* openssl per socket struct */ 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__); clicon_debug(1, "%s", __FUNCTION__);
/* Extract socket parameters from single socket config: ns, addr, port, ssl */ /* Extract socket parameters from single socket config: ns, addr, port, ssl */
if (restconf_socket_extract(h, xs, nsc, &netns, &address, &addrtype, &port, &ssl, &callhome) < 0) if (restconf_socket_extract(h, xs, nsc, &description, &netns, &address, &addrtype, &port, &ssl, &callhome, &periodic, &period, &max_attempts) < 0)
goto done; goto done;
/* /*
@ -1124,6 +742,16 @@ openssl_init_socket(clicon_handle h,
rsock->rs_h = h; rsock->rs_h = h;
rsock->rs_ssl = ssl; /* true/false */ rsock->rs_ssl = ssl; /* true/false */
rsock->rs_callhome = callhome; 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");
goto done;
}
if (callhome){ if (callhome){
if (!ssl){ if (!ssl){
clicon_err(OE_SSL, EINVAL, "Restconf callhome requires SSL"); clicon_err(OE_SSL, EINVAL, "Restconf callhome requires SSL");
@ -1159,8 +787,8 @@ openssl_init_socket(clicon_handle h,
INSQ(rsock, rh->rh_sockets); INSQ(rsock, rh->rh_sockets);
if (callhome){ if (callhome){
rsock->rs_ss = -1; /* Not applicable fro callhome */ rsock->rs_ss = -1; /* Not applicable from callhome */
if (restconf_callhome_timer(0, rsock) < 0) if (restconf_callhome_timer(rsock, 0) < 0)
goto done; goto done;
} }
else { else {

View file

@ -167,8 +167,9 @@ restconf_stream_free(restconf_stream_data *sd)
/*! Create restconf connection struct /*! Create restconf connection struct
*/ */
restconf_conn * restconf_conn *
restconf_conn_new(clicon_handle h, restconf_conn_new(clicon_handle h,
int s) int s,
restconf_socket *rsock)
{ {
restconf_conn *rc; restconf_conn *rc;
@ -179,6 +180,7 @@ restconf_conn_new(clicon_handle h,
memset(rc, 0, sizeof(restconf_conn)); memset(rc, 0, sizeof(restconf_conn));
rc->rc_h = h; rc->rc_h = h;
rc->rc_s = s; rc->rc_s = s;
rc->rc_socket = rsock;
return rc; return rc;
} }
@ -337,11 +339,12 @@ restconf_connection_sanity(clicon_handle h,
* see also this function in restcont_api_openssl.c * see also this function in restcont_api_openssl.c
*/ */
static int static int
native_buf_write(clicon_handle h, native_buf_write(clicon_handle h,
char *buf, char *buf,
size_t buflen, size_t buflen,
int s, int s,
SSL *ssl) SSL *ssl,
restconf_socket *rsock)
{ {
int retval = -1; int retval = -1;
ssize_t len; ssize_t len;
@ -376,7 +379,7 @@ native_buf_write(clicon_handle h,
if (ssl){ if (ssl){
SSL_free(ssl); SSL_free(ssl);
} }
if (restconf_connection_close(h, s) < 0) if (restconf_connection_close(h, s, rsock) < 0)
goto done; goto done;
goto ok; /* Close socket and ssl */ goto ok; /* Close socket and ssl */
} }
@ -408,7 +411,7 @@ native_buf_write(clicon_handle h,
break; break;
case ECONNRESET: /* Connection reset by peer */ case ECONNRESET: /* Connection reset by peer */
case EPIPE: /* Broken pipe */ case EPIPE: /* Broken pipe */
if (restconf_connection_close(h, s) < 0) if (restconf_connection_close(h, s, rsock) < 0)
goto done; goto done;
goto ok; /* Close socket and ssl */ goto ok; /* Close socket and ssl */
break; break;
@ -436,12 +439,13 @@ native_buf_write(clicon_handle h,
* @param[in] body If given add message body using media * @param[in] body If given add message body using media
* @see restconf_badrequest which can only be called in a request context * @see restconf_badrequest which can only be called in a request context
*/ */
int static int
native_send_badrequest(clicon_handle h, native_send_badrequest(clicon_handle h,
int s, int s,
SSL *ssl, SSL *ssl,
char *media, char *media,
char *body) char *body,
restconf_socket *rsock)
{ {
int retval = -1; int retval = -1;
cbuf *cb = NULL; cbuf *cb = NULL;
@ -461,7 +465,7 @@ native_send_badrequest(clicon_handle h,
cprintf(cb, "\r\n"); cprintf(cb, "\r\n");
if (body) if (body)
cprintf(cb, "%s\r\n", body); cprintf(cb, "%s\r\n", body);
if (native_buf_write(h, cbuf_get(cb), cbuf_len(cb), s, ssl) < 0) if (native_buf_write(h, cbuf_get(cb), cbuf_len(cb), s, ssl, rsock) < 0) // XXX rsock
goto done; goto done;
retval = 0; retval = 0;
done: done:
@ -574,7 +578,7 @@ read_regular(restconf_conn *rc,
switch(errno){ switch(errno){
case ECONNRESET:/* Connection reset by peer */ case ECONNRESET:/* Connection reset by peer */
clicon_debug(1, "%s %d Connection reset by peer", __FUNCTION__, rc->rc_s); clicon_debug(1, "%s %d Connection reset by peer", __FUNCTION__, rc->rc_s);
if (restconf_connection_close(rc->rc_h, rc->rc_s) < 0) if (restconf_connection_close(rc->rc_h, rc->rc_s, rc->rc_socket) < 0)
goto done; goto done;
restconf_conn_free(rc); restconf_conn_free(rc);
retval = 0; /* Close socket and ssl */ retval = 0; /* Close socket and ssl */
@ -675,7 +679,7 @@ restconf_http1_process(restconf_conn *rc,
goto done; goto done;
} }
cprintf(cberr, "<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><error><error-type>protocol</error-type><error-tag>malformed-message</error-tag><error-message>%s</error-message></error></errors>", clicon_err_reason); cprintf(cberr, "<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><error><error-type>protocol</error-type><error-tag>malformed-message</error-tag><error-message>%s</error-message></error></errors>", clicon_err_reason);
if (native_send_badrequest(h, rc->rc_s, rc->rc_ssl, "application/yang-data+xml", cbuf_get(cberr)) < 0) if (native_send_badrequest(h, rc->rc_s, rc->rc_ssl, "application/yang-data+xml", cbuf_get(cberr), rc->rc_socket) < 0)
goto done; goto done;
goto ok; goto ok;
} }
@ -686,7 +690,7 @@ restconf_http1_process(restconf_conn *rc,
goto done; goto done;
if (ret == 1){ if (ret == 1){
if (native_buf_write(h, cbuf_get(sd->sd_outp_buf), cbuf_len(sd->sd_outp_buf), if (native_buf_write(h, cbuf_get(sd->sd_outp_buf), cbuf_len(sd->sd_outp_buf),
rc->rc_s, rc->rc_ssl) < 0) rc->rc_s, rc->rc_ssl, rc->rc_socket) < 0)
goto done; goto done;
cvec_reset(sd->sd_outp_hdrs); cvec_reset(sd->sd_outp_hdrs);
cbuf_reset(sd->sd_outp_buf); cbuf_reset(sd->sd_outp_buf);
@ -714,7 +718,7 @@ restconf_http1_process(restconf_conn *rc,
if (restconf_http1_path_root(h, rc) < 0) if (restconf_http1_path_root(h, rc) < 0)
goto done; goto done;
if (native_buf_write(h, cbuf_get(sd->sd_outp_buf), cbuf_len(sd->sd_outp_buf), if (native_buf_write(h, cbuf_get(sd->sd_outp_buf), cbuf_len(sd->sd_outp_buf),
rc->rc_s, rc->rc_ssl) < 0) rc->rc_s, rc->rc_ssl, rc->rc_socket) < 0)
goto done; goto done;
cvec_reset(sd->sd_outp_hdrs); /* Can be done in native_send_reply */ cvec_reset(sd->sd_outp_hdrs); /* Can be done in native_send_reply */
cbuf_reset(sd->sd_outp_buf); cbuf_reset(sd->sd_outp_buf);
@ -723,7 +727,7 @@ restconf_http1_process(restconf_conn *rc,
if (rc->rc_exit){ /* Server-initiated exit */ if (rc->rc_exit){ /* Server-initiated exit */
SSL_free(rc->rc_ssl); SSL_free(rc->rc_ssl);
rc->rc_ssl = NULL; rc->rc_ssl = NULL;
if (restconf_connection_close(h, rc->rc_s) < 0) if (restconf_connection_close(h, rc->rc_s, rc->rc_socket) < 0)
goto done; goto done;
restconf_conn_free(rc); restconf_conn_free(rc);
retval = 0; retval = 0;
@ -869,6 +873,8 @@ restconf_native_handle_get(clicon_handle h)
return NULL; return NULL;
} }
/*---------------------------- Connect ---------------------------------*/
/*! New data connection after accept, receive and reply on data socket /*! New data connection after accept, receive and reply on data socket
* *
* @param[in] s Socket where message arrived. read from this. * @param[in] s Socket where message arrived. read from this.
@ -959,37 +965,491 @@ restconf_connection(int s,
return retval; return retval;
} /* restconf_connection */ } /* restconf_connection */
int restconf_callhome_timer(int fd, void *arg); // XXX FIX HEADERS INSTEAD
/*! Close Restconf native connection socket and unregister callback /*! Close Restconf native connection socket and unregister callback
* For callhome also start reconnect timer * For callhome also start reconnect timer
*/ */
int int
restconf_connection_close(clicon_handle h, restconf_connection_close(clicon_handle h,
int s) int s,
restconf_socket *rsock)
{ {
int retval = -1; int retval = -1;
restconf_native_handle *rh = NULL;
restconf_socket *rsock;
clicon_debug(1, "%s", __FUNCTION__);
if (close(s) < 0){ if (close(s) < 0){
clicon_err(OE_UNIX, errno, "close"); clicon_err(OE_UNIX, errno, "close");
goto done; goto done;
} }
clixon_event_unreg_fd(s, restconf_connection); clixon_event_unreg_fd(s, restconf_connection);
/* re-set timer */
if (rsock->rs_callhome &&
restconf_callhome_timer(rsock, 1) < 0)
goto done;
retval = 0;
done:
clicon_debug(1, "%s %d", __FUNCTION__, retval);
return retval;
}
/*------------------------------ Accept--------------------------------*/
if (0 && (rh = restconf_native_handle_get(h)) != NULL){ /*! Check ALPN result
while ((rsock = rh->rh_sockets) != NULL) * @proto[out] proto
if (rsock->rs_callhome && s == rsock->rs_ss){ * @retval 1 OK with proto set
/* Maybe we should have a special wrapper function that only sets timer here? */ * @retval 0 Fail, ALPN null or not recognized
if (restconf_callhome_timer(0, rsock) < 0) * @retval -1 Error
goto done; */
break; static int
} ssl_alpn_check(clicon_handle h,
const unsigned char *alpn,
unsigned int alpnlen,
restconf_conn *rc,
restconf_http_proto *proto)
{
int retval = -1;
int ret;
cbuf *cberr = NULL;
clicon_debug(1, "%s", __FUNCTION__);
/* Alternatively, call restconf_str2proto but alpn is not a proper string */
if (alpn && alpnlen == 8 && memcmp("http/1.1", alpn, 8) == 0){
*proto = HTTP_11;
retval = 1; /* http/1.1 */
goto done;
} }
#ifdef HAVE_LIBNGHTTP2
else if (alpn && alpnlen == 2 && memcmp("h2", alpn, 2) == 0){
*proto = HTTP_2;
retval = 1; /* http/2 */
goto done;
}
#endif
else {
if ((cberr = cbuf_new()) == NULL){
clicon_err(OE_UNIX, errno, "cbuf_new");
goto done;
}
if (alpn != NULL){
cprintf(cberr, "<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><error><error-type>protocol</error-type><error-tag>malformed-message</error-tag><error-message>ALPN: protocol not recognized: %s</error-message></error></errors>", alpn);
clicon_log(LOG_INFO, "%s Warning: %s", __FUNCTION__, cbuf_get(cberr));
if (native_send_badrequest(h, rc->rc_s, rc->rc_ssl,
"application/yang-data+xml",
cbuf_get(cberr), rc->rc_socket) < 0)
goto done;
}
else{
/* XXX Sending badrequest here gives a segv in SSL_shutdown() later or a SIGPIPE here */
clicon_log(LOG_INFO, "%s Warning: ALPN: No protocol selected, or no ALPN?", __FUNCTION__);
}
if (rc->rc_ssl){
/* nmap ssl-known-key SEGV at s->method->ssl_shutdown(s);
* OR OpenSSL error: : SSL_shutdown, err: SSL_ERROR_SYSCALL(5)
*/
if ((ret = SSL_shutdown(rc->rc_ssl)) < 0){
int e = SSL_get_error(rc->rc_ssl, ret);
if (e == SSL_ERROR_SYSCALL){
clicon_log(LOG_INFO, "%s Warning: SSL_shutdown SSL_ERROR_SYSCALL", __FUNCTION__);
/* Continue */
}
else {
clicon_err(OE_SSL, 0, "SSL_shutdown, err:%d", e);
goto done;
}
}
SSL_free(rc->rc_ssl);
}
restconf_conn_free(rc);
}
retval = 0; /* ALPN not OK */
done:
clicon_debug(1, "%s retval:%d", __FUNCTION__, retval);
if (cberr)
cbuf_free(cberr);
return retval;
} /* ssl_alpn_check */
/*! Accept new socket client. Note SSL not ip, this applies also to callhome
* @param[in] h Clixon handle
* @param[in] s Socket (unix or ip)
* @param[in] rsock Socket struct
* @see openssl_init_socket where this callback is registered
*/
int
restconf_ssl_accept_client(clicon_handle h,
int s,
restconf_socket *rsock)
{
int retval = -1;
restconf_native_handle *rh = NULL;
restconf_conn *rc = NULL;
char *name = NULL;
int ret;
int e;
int er;
int readmore;
const unsigned char *alpn = NULL;
unsigned int alpnlen = 0;
restconf_http_proto proto = HTTP_11; /* Non-SSL negotiation NYI */
clicon_debug(1, "%s", __FUNCTION__);
#ifdef HAVE_LIBNGHTTP2
#ifndef HAVE_HTTP1
proto = HTTP_2; /* If nghttp2 only let default be 2.0 */
#endif
#endif
if ((rh = restconf_native_handle_get(h)) == NULL){
clicon_err(OE_XML, EFAULT, "No openssl handle");
goto done;
}
/*
* Register callbacks for actual data socket
*/
if ((rc = restconf_conn_new(h, s, rsock)) == NULL)
goto done;
clicon_debug(1, "%s s:%d", __FUNCTION__, rc->rc_s);
if (rsock->rs_ssl){
if ((rc->rc_ssl = SSL_new(rh->rh_ctx)) == NULL){
clicon_err(OE_SSL, 0, "SSL_new");
goto done;
}
clicon_debug(1, "%s SSL_new(%p)", __FUNCTION__, rc->rc_ssl);
/* CCL_CTX_set_verify already set, need not call SSL_set_verify again for this server
*/
/* X509_CHECK_FLAG_NO_WILDCARDS disables wildcard expansion */
SSL_set_hostflags(rc->rc_ssl, X509_CHECK_FLAG_NO_WILDCARDS);
#if 0
/* XXX This code is kept for the time being just for reference, it does not belong here.
* If you want to restrict client certs to a specific set.
* Otherwise this is done in restcon ca-auth callback and ultimately NACM
* SSL_set1_host() sets the expected DNS hostname to name
C = SE
L = Stockholm
O = Clixon
OU = clixon
CN = ca <---
emailAddress = olof@hagsand.se
*/
if (SSL_set1_host(rc->rc_ssl, "andy") != 1) { /* for peer cert */
clicon_err(OE_SSL, 0, "SSL_set1_host");
goto done;
}
if (SSL_add1_host(rc->rc_ssl, "olof") != 1) { /* for peer cert */
clicon_err(OE_SSL, 0, "SSL_set1_host");
goto done;
}
#endif
if (SSL_set_fd(rc->rc_ssl, rc->rc_s) != 1){
clicon_err(OE_SSL, 0, "SSL_set_fd");
goto done;
}
readmore = 1;
while (readmore){
readmore = 0;
/* 1: OK, -1 fatal, 0: TLS/SSL handshake was not successful
* Both error cases: Call SSL_get_error() with the return value ret
*/
if ((ret = SSL_accept(rc->rc_ssl)) != 1) {
clicon_debug(1, "%s SSL_accept() ret:%d errno:%d", __FUNCTION__, ret, er=errno);
e = SSL_get_error(rc->rc_ssl, ret);
switch (e){
case SSL_ERROR_SSL: /* 1 */
clicon_debug(1, "%s SSL_ERROR_SSL (non-ssl message on ssl socket)", __FUNCTION__);
#if 1
if (native_send_badrequest(h, rc->rc_s, NULL, "application/yang-data+xml",
"<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><error><error-type>protocol</error-type><error-tag>malformed-message</error-tag><error-message>The plain HTTP request was sent to HTTPS port</error-message></error></errors>", rc->rc_socket) < 0)
goto done;
#endif
SSL_free(rc->rc_ssl);
rc->rc_ssl = NULL;
if (restconf_connection_close(h, rc->rc_s, rc->rc_socket) < 0)
goto done;
restconf_conn_free(rc);
goto ok;
break;
case SSL_ERROR_SYSCALL: /* 5 */
/* Some non-recoverable, fatal I/O error occurred. The OpenSSL error queue
may contain more information on the error. For socket I/O on Unix systems,
consult errno for details. If this error occurs then no further I/O
operations should be performed on the connection and SSL_shutdown() must
not be called.*/
clicon_debug(1, "%s SSL_accept() SSL_ERROR_SYSCALL %d", __FUNCTION__, er);
if (restconf_close_ssl_socket(rc, 0) < 0)
goto done;
restconf_conn_free(rc);
rc = NULL;
goto ok;
break;
case SSL_ERROR_WANT_READ: /* 2 */
case SSL_ERROR_WANT_WRITE: /* 3 */
/* SSL_ERROR_WANT_READ is returned when the last operation was a read operation
* from a nonblocking BIO.
* That is, it can happen if restconf_socket_init() below is called
* with SOCK_NONBLOCK
*/
clicon_debug(1, "%s write SSL_ERROR_WANT_READ", __FUNCTION__);
usleep(10000);
readmore = 1;
break;
case SSL_ERROR_NONE: /* 0 */
case SSL_ERROR_ZERO_RETURN: /* 6 */
case SSL_ERROR_WANT_CONNECT: /* 7 */
case SSL_ERROR_WANT_ACCEPT: /* 8 */
case SSL_ERROR_WANT_X509_LOOKUP: /* 4 */
case SSL_ERROR_WANT_ASYNC: /* 8 */
case SSL_ERROR_WANT_ASYNC_JOB: /* 10 */
#ifdef SSL_ERROR_WANT_CLIENT_HELLO_CB
case SSL_ERROR_WANT_CLIENT_HELLO_CB: /* 11 */
#endif
default:
clicon_err(OE_SSL, 0, "SSL_accept:%d", e);
goto done;
break;
}
} /* SSL_accept */
} /* while(readmore) */
/* Sets data and len to point to the client's requested protocol for this connection. */
#ifndef OPENSSL_NO_NEXTPROTONEG
SSL_get0_next_proto_negotiated(rc->rc_ssl, &alpn, &alpnlen);
#endif /* !OPENSSL_NO_NEXTPROTONEG */
if (alpn == NULL) {
/* Returns a pointer to the selected protocol in data with length len. */
SSL_get0_alpn_selected(rc->rc_ssl, &alpn, &alpnlen);
}
if ((ret = ssl_alpn_check(h, alpn, alpnlen, rc, &proto)) < 0)
goto done;
if (ret == 0)
goto ok;
clicon_debug(1, "%s proto:%s", __FUNCTION__, restconf_proto2str(proto));
#if 0 /* Seems too early to fail here, instead let authentication callback deal with this */
/* For client-cert authentication, check if any certs are present,
* if not, send bad request
* Alt: set SSL_CTX_set_verify(ctx, SSL_VERIFY_FAIL_IF_NO_PEER_CERT)
* but then SSL_accept fails.
*/
if (restconf_auth_type_get(h) == CLIXON_AUTH_CLIENT_CERTIFICATE){
X509 *peercert;
if ((peercert = SSL_get_peer_certificate(rc->rc_ssl)) != NULL){
X509_free(peercert);
}
else { /* Get certificates (if available) */
if (proto != HTTP_2 &&
native_send_badrequest(h, rc->rc_s, rc->rc_ssl, "application/yang-data+xml",
"<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\"><error><error-type>protocol</error-type><error-tag>malformed-message</error-tag><error-message>Peer certificate required</error-message></error></errors>", rc->rc_socket) < 0)
goto done;
restconf_conn_free(rc);
if (rc->rc_ssl){
if ((ret = SSL_shutdown(rc->rc_ssl)) < 0){
int e = SSL_get_error(rc->rc_ssl, ret);
clicon_err(OE_SSL, 0, "SSL_shutdown, err:%d", e);
goto done;
}
SSL_free(rc->rc_ssl);
rc->rc_ssl = NULL;
}
goto ok;
}
}
#endif
/* Get the actual peer, XXX this maybe could be done in ca-auth client-cert code ?
* Note this _only_ works if SSL_set1_host() was set previously,...
*/
if ((ret = SSL_get_verify_result(rc->rc_ssl)) == X509_V_OK) { /* for peer cert */
const char *peername = SSL_get0_peername(rc->rc_ssl);
if (peername != NULL) {
/* Name checks were in scope and matched the peername */
clicon_debug(1, "%s peername:%s", __FUNCTION__, peername);
}
}
#if 0
else{
clicon_log(LOG_NOTICE, "Cert error: %s", X509_verify_cert_error_string(ret));
/* Maybe should return already here, but to get proper return message need to
* continue to http/1 or http/2 handling
* @see restconf_connection_sanity
*/
}
#endif
#if 0 /* debug */
if (clicon_debug_get())
restconf_listcerts(rc->rc_ssl);
#endif
} /* if ssl */
rc->rc_proto = proto;
switch (rc->rc_proto){
#ifdef HAVE_HTTP1
case HTTP_10:
case HTTP_11:
/* Create a default stream for http/1 */
if (restconf_stream_data_new(rc, 0) == NULL)
goto done;
break;
#endif /* HAVE_HTTP1 */
#ifdef HAVE_LIBNGHTTP2
case HTTP_2:{
if (http2_session_init(rc) < 0){
restconf_close_ssl_socket(rc, 1);
goto done;
}
if (http2_send_server_connection(rc) < 0){
restconf_close_ssl_socket(rc, 1);
#ifdef NYI
if (ssl) {
SSL_shutdown(ssl);
}
bufferevent_free(session_data->bev);
nghttp2_session_del(session_data->session);
for (stream_data = session_data->root.next; stream_data;) {
http2_stream_data *next = stream_data->next;
delete_http2_stream_data(stream_data);
stream_data = next;
}
free(session_data->client_addr);
free(session_data);
#endif
goto done;
}
break;
}
#endif /* HAVE_LIBNGHTTP2 */
default:
break;
} /* switch proto */
if (clixon_event_reg_fd(rc->rc_s, restconf_connection, (void*)rc, "restconf client socket") < 0)
goto done;
ok:
retval = 0;
done:
clicon_debug(1, "%s retval %d", __FUNCTION__, retval);
if (name)
free(name);
return retval;
} /* restconf_ssl_accept_client */
/*! Callhome timer callback
*
* @param[in] fd No-op
* @param[in] arg restconf_socket
* Can be called directly, but typically call wrapper restconf_callhome_timer instead
* the reason is that this function may continue with SSL accept handling whereas the wrapper always
* returns directly.
*/
int
restconf_callhome_cb(int fd,
void *arg)
{
int retval = -1;
clicon_handle h;
restconf_socket *rsock = NULL;
struct sockaddr_in6 sin6 = {0,}; // because its larger than sin and sa
struct sockaddr *sa = (struct sockaddr *)&sin6;
size_t sa_len;
int s;
if ((rsock = (restconf_socket *)arg) == NULL){
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__);
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)
goto done;
if ((s = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) {
clicon_err(OE_UNIX, errno, "socket");
goto done;
}
clicon_debug(1, "%s connect %hu", __FUNCTION__, rsock->rs_port);
if (connect(s, sa, sa_len) < 0){
clicon_debug(1, "%s connect:%d %s", __FUNCTION__, errno, strerror(errno));
close(s);
rsock->rs_attempts++;
rsock->rs_ss = -1;
/* Fail: Initiate new timer */
if (restconf_callhome_timer(rsock, 0) < 0)
goto done;
}
else {
rsock->rs_ss = s;
rsock->rs_attempts = 0;
if (restconf_ssl_accept_client(h, s, rsock) < 0)
goto done;
}
clicon_debug(1, "%s connect done", __FUNCTION__);
retval = 0; retval = 0;
done: done:
return retval; return retval;
} }
/*! 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)
*/
int
restconf_callhome_timer(restconf_socket *rsock,
int new)
{
int retval = -1;
struct timeval now;
struct timeval t;
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->rs_callhome)
goto ok; /* shouldnt happen */
gettimeofday(&now, NULL);
if (rsock->rs_periodic){
if (new || 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;
while (t1.tv_sec < now.tv_sec)
t1.tv_sec += rsock->rs_period;
t = t1;
}
else {
t1.tv_sec = 1;
timeradd(&now, &t1, &t);
}
}
else{ /* persistent: try again: attempts? */
t1.tv_sec = 1;
timeradd(&now, &t1, &t);
}
if ((cb = cbuf_new()) == NULL){
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");
if (rsock->rs_description)
clicon_debug(1, "%s registering %s: %lu", __FUNCTION__, rsock->rs_description, t.tv_sec);
else
clicon_debug(1, "%s: %lu", __FUNCTION__, t.tv_sec);
/* Should be only place restconf_callhome_cb is registered */
if (clixon_event_reg_timeout(t,
restconf_callhome_cb,
rsock,
cbuf_get(cb)) < 0)
goto done;
ok:
retval = 0;
done:
if (cb)
cbuf_free(cb);
return retval;
}

View file

@ -85,28 +85,6 @@ typedef struct {
uint8_t *sd_settings2; /* Settings for upgrade to http/2 request */ uint8_t *sd_settings2; /* Settings for upgrade to http/2 request */
} restconf_stream_data; } restconf_stream_data;
/* Restconf connection handle
* Per connection request
*/
typedef struct restconf_conn {
/* XXX rc_proto and rc_proto_d1/d2 may not both be necessary.
* remove rc_proto?
*/
restconf_http_proto rc_proto; /* HTTP protocol: http/1 or http/2 */
int rc_proto_d1; /* parsed version digit 1 */
int rc_proto_d2; /* parsed version digit 2 */
int rc_s; /* Connection socket */
clicon_handle rc_h; /* Clixon handle */
SSL *rc_ssl; /* Structure for SSL connection */
restconf_stream_data *rc_streams; /* List of http/2 session streams */
int rc_exit; /* Set to close socket server-side */
/* Decision to keep lib-specific data here, otherwise new struct necessary
* drawback is specific includes need to go everywhere */
#ifdef HAVE_LIBNGHTTP2
nghttp2_session *rc_ngsession; /* XXX Not sure it is needed */
#endif
} restconf_conn;
/* Restconf per socket handle /* Restconf per socket handle
* Two types: listen and callhome. * Two types: listen and callhome.
* Listen: Uses socket rs_ss to listen for connections and accepts them, creates one * Listen: Uses socket rs_ss to listen for connections and accepts them, creates one
@ -117,6 +95,7 @@ typedef struct restconf_conn {
typedef struct { typedef struct {
qelem_t rs_qelem; /* List header */ qelem_t rs_qelem; /* List header */
clicon_handle rs_h; /* Clixon handle */ clicon_handle rs_h; /* Clixon handle */
char *rs_description; /* Description */
int rs_callhome; /* 0: listen, 1: callhome */ int rs_callhome; /* 0: listen, 1: callhome */
int rs_ss; /* Listen: Server socket, ready for accept int rs_ss; /* Listen: Server socket, ready for accept
* Callhome: connect socket (same as restconf_conn->rc_s) */ * Callhome: connect socket (same as restconf_conn->rc_s) */
@ -125,8 +104,42 @@ typedef struct {
eg inet:ipv4-address or inet:ipv6-address */ eg inet:ipv4-address or inet:ipv6-address */
char *rs_addrstr; /* Address as string, eg 127.0.0.1, ::1 */ char *rs_addrstr; /* Address as string, eg 127.0.0.1, ::1 */
uint16_t rs_port; /* Protocol port */ uint16_t rs_port; /* Protocol port */
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) */
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*/
uint8_t rs_attempts; /* Dynamic connect attempts in this round (if callhome)
* Set in restconf_callhome_cb
*/
} restconf_socket; } restconf_socket;
/* Restconf connection handle
* Per connection request
*/
typedef struct restconf_conn {
/* XXX rc_proto and rc_proto_d1/d2 may not both be necessary.
* remove rc_proto?
*/
restconf_http_proto rc_proto; /* HTTP protocol: http/1 or http/2 */
int rc_proto_d1; /* parsed version digit 1 */
int rc_proto_d2; /* parsed version digit 2 */
int rc_s; /* Connection socket */
clicon_handle rc_h; /* Clixon handle */
SSL *rc_ssl; /* Structure for SSL connection */
restconf_stream_data *rc_streams; /* List of http/2 session streams */
int rc_exit; /* Set to close socket server-side */
/* Decision to keep lib-specific data here, otherwise new struct necessary
* drawback is specific includes need to go everywhere */
#ifdef HAVE_LIBNGHTTP2
nghttp2_session *rc_ngsession; /* XXX Not sure it is needed */
#endif
restconf_socket *rc_socket; /* Backpointer to restconf_socket needed for callhome */
} restconf_conn;
/* Restconf handle /* Restconf handle
* Global data about ssl (not per packet/request) * Global data about ssl (not per packet/request)
*/ */
@ -142,16 +155,19 @@ typedef struct {
restconf_stream_data *restconf_stream_data_new(restconf_conn *rc, int32_t stream_id); restconf_stream_data *restconf_stream_data_new(restconf_conn *rc, int32_t stream_id);
restconf_stream_data *restconf_stream_find(restconf_conn *rc, int32_t id); restconf_stream_data *restconf_stream_find(restconf_conn *rc, int32_t id);
int restconf_stream_free(restconf_stream_data *sd); int restconf_stream_free(restconf_stream_data *sd);
restconf_conn *restconf_conn_new(clicon_handle h, int s); restconf_conn *restconf_conn_new(clicon_handle h, int s, restconf_socket *socket);
int restconf_conn_free(restconf_conn *rc); int restconf_conn_free(restconf_conn *rc);
int ssl_x509_name_oneline(SSL *ssl, char **oneline); int ssl_x509_name_oneline(SSL *ssl, char **oneline);
int restconf_close_ssl_socket(restconf_conn *rc, int shutdown); /* XXX in restconf_main_native.c */ int restconf_close_ssl_socket(restconf_conn *rc, int shutdown); /* XXX in restconf_main_native.c */
int restconf_connection_sanity(clicon_handle h, restconf_conn *rc, restconf_stream_data *sd); int restconf_connection_sanity(clicon_handle h, restconf_conn *rc, restconf_stream_data *sd);
int native_send_badrequest(clicon_handle h, int s, SSL *ssl, char *media, char *body);
restconf_native_handle *restconf_native_handle_get(clicon_handle h); restconf_native_handle *restconf_native_handle_get(clicon_handle h);
int restconf_connection(int s, void *arg); int restconf_connection(int s, void *arg);
int restconf_connection_close(clicon_handle h, int s); 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);
#endif /* _RESTCONF_NATIVE_H_ */ #endif /* _RESTCONF_NATIVE_H_ */

View file

@ -2,6 +2,13 @@
# test of Restconf callhome # test of Restconf callhome
# See RFC 8071 NETCONF Call Home and RESTCONF Call Home # See RFC 8071 NETCONF Call Home and RESTCONF Call Home
# Simple NACM for single "andy" user # Simple NACM for single "andy" user
# The client is clixon_restconf_callhome_client that waits for accept, connects, sends a GET immediately,
# closes the socket and re-listens
# The server opens three sockets:
# 1) regular listen socket for setting init value
# 2) persistent socket
# 3) periodic socket (10s) port 8336
# XXX periodic: idle-timeout not tested
# Magic line must be first in script (see README.md) # Magic line must be first in script (see README.md)
s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi
@ -19,6 +26,10 @@ cfg=$dir/conf_yang.xml
fyang=$dir/clixon-example.yang fyang=$dir/clixon-example.yang
clispec=$dir/spec.cli clispec=$dir/spec.cli
# HTTP request client->server
frequest=$dir/frequest
# HTTP expected reply server->client
certdir=$dir/certs certdir=$dir/certs
cakey=$certdir/ca_key.pem cakey=$certdir/ca_key.pem
cacert=$certdir/ca_cert.pem cacert=$certdir/ca_cert.pem
@ -60,17 +71,39 @@ cat <<EOF > $cfg
<server-ca-cert-path>$cacert</server-ca-cert-path> <server-ca-cert-path>$cacert</server-ca-cert-path>
<debug>1</debug> <debug>1</debug>
<socket> <socket>
<description>callhome persistent</description>
<namespace>default</namespace> <namespace>default</namespace>
<call-home> <call-home>
<connection-type> <connection-type>
<persistent/> <persistent/>
</connection-type> </connection-type>
<reconnect-strategy>
<max-attempts>3</max-attempts>
</reconnect-strategy>
</call-home> </call-home>
<address>127.0.0.1</address> <address>127.0.0.1</address>
<port>4336</port> <port>4336</port>
<ssl>true</ssl> <ssl>true</ssl>
</socket> </socket>
<socket> <socket>
<description>callhome periodic</description>
<namespace>default</namespace>
<call-home>
<connection-type>
<periodic>
<period>10</period>
</periodic>
</connection-type>
<reconnect-strategy>
<max-attempts>3</max-attempts>
</reconnect-strategy>
</call-home>
<address>127.0.0.1</address>
<port>8336</port>
<ssl>true</ssl>
</socket>
<socket>
<description>listen</description>
<namespace>default</namespace> <namespace>default</namespace>
<address>0.0.0.0</address> <address>0.0.0.0</address>
<port>443</port> <port>443</port>
@ -219,14 +252,16 @@ cat <<EOF > $dir/startup_db
</${DATASTORE_TOP}> </${DATASTORE_TOP}>
EOF EOF
# Callhome request from client # Callhome request from client->server
cat <<EOF > $dir/data cat <<EOF > $frequest
GET /restconf/data/clixon-example:table HTTP/$HVERCH GET /restconf/data/clixon-example:table HTTP/$HVERCH
Host: localhost Host: localhost
Accept: application/yang-data+xml Accept: application/yang-data+xml
EOF EOF
expectreply="<table xmlns=\"urn:example:clixon\"><parameter><name>x</name><value>foo</value></parameter></table>"
new "test params: -f $cfg" new "test params: -f $cfg"
# Bring your own backend # Bring your own backend
if [ $BE -ne 0 ]; then if [ $BE -ne 0 ]; then
@ -249,7 +284,7 @@ if [ $RC -ne 0 ]; then
stop_restconf_pre stop_restconf_pre
new "start restconf daemon" new "start restconf daemon"
start_restconf -f $cfg start_restconf -f $cfg -D 1 -l s
fi fi
new "wait restconf" new "wait restconf"
@ -258,9 +293,39 @@ wait_restconf
new "restconf Add init data" 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" 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"
new "Send GET via callhome client" # tests for (one) well-formed HTTP status and three replies and that the time is reasonable
echo "${clixon_restconf_callhome_client} -D $DBG -f $dir/data -a 127.0.0.1 -c $srvcert -k $srvkey -C $cacert" t0=$(date +"%s")
expectpart "$(${clixon_restconf_callhome_client} -D $DBG -f $dir/data -a 127.0.0.1 -c $srvcert -k $srvkey -C $cacert)" 0 "HTTP/$HVERCH 200 OK" "Content-Type: application/yang-data+xml" 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"
t1=$(date +"%s")
let t=t1-t0
new "Check persistent interval ($t) is in interval [2,4]"
if [ $t -lt 2 -o $t -ge 4 ]; then
err1 "timer in interval [2,4] but is: $t"
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
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"
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"
t1=$(date +"%s")
let t=t1-t0
new "Check persistent interval ($t) is in interval [2,4]"
if [ $t -lt 2 -o $t -ge 4 ]; then
err1 "timer in interval [2,4] but is: $t"
fi
# Kill old # Kill old
if [ $RC -ne 0 ]; then if [ $RC -ne 0 ]; then

View file

@ -41,6 +41,8 @@
The callhome-client listens on accept, when connect comes in, creates data socket and sends The callhome-client listens on accept, when connect comes in, creates data socket and sends
RESTCONF GET to server, then re-waits for new accepts. RESTCONF GET to server, then re-waits for new accepts.
When accepting a connection, send HTTP data from input or -f <file> tehn wait for reply
Reply is matched with -e <expectfile> or printed on stdout
*/ */
#ifdef HAVE_CONFIG_H #ifdef HAVE_CONFIG_H
@ -66,7 +68,7 @@
/* clixon */ /* clixon */
#include "clixon/clixon.h" #include "clixon/clixon.h"
#define UTIL_TLS_OPTS "hD:f:F:a:p:c:C:k:n:" #define UTIL_TLS_OPTS "hD:f:e:F:a:p:c:C:k:n:"
#define RESTCONF_CH_TLS 4336 #define RESTCONF_CH_TLS 4336
@ -143,18 +145,63 @@ callhome_bind(struct sockaddr *sa,
return retval; return retval;
} }
/*! Client data socket /*! read data from file return a malloced buffer
* Note same file is reread multiple times: same request/reply is made each iteration
* Also, the file read is limited to 1024 bytes
*/ */
static int static int
tls_input_cb(int s, read_data_file(FILE *fe,
void *arg) char **bufp,
size_t *lenp)
{ {
int retval = -1; int retval = -1;
char *buf = NULL;
int buflen = 1024; /* start size */
char ch;
size_t len = 0;
int ret;
if ((buf = malloc(buflen)) == NULL){
clicon_err(OE_UNIX, errno, "malloc");
goto done;
}
memset(buf, 0, buflen);
/* Start file form beginning */
rewind(fe);
while (1){
if ((ret = fread(&ch, 1, 1, fe)) < 0){
clicon_err(OE_JSON, errno, "fread");
goto done;
}
if (ret == 0)
break;
buf[len++] = ch;
// XXX No realloc, can overflow
}
*bufp = buf;
*lenp = len;
retval = 0;
done:
return retval;
}
/*! Client data socket, receive reply from server
*/
static int
tls_server_reply_cb(int s,
void *arg)
{
int retval = -1;
tls_session_data *sd = (tls_session_data *)arg; tls_session_data *sd = (tls_session_data *)arg;
SSL *ssl; SSL *ssl;
char buf[1024]; char buf[1024];
int n; int n;
char *expbuf = NULL;
struct timeval tn;
struct timeval td;
static int seq = 1;
static struct timeval t0 = {0,};
clicon_debug(1, "%s", __FUNCTION__); clicon_debug(1, "%s", __FUNCTION__);
ssl = sd->sd_ssl; ssl = sd->sd_ssl;
/* get reply & decrypt */ /* get reply & decrypt */
@ -167,10 +214,14 @@ tls_input_cb(int s,
goto done; goto done;
} }
buf[n] = 0; buf[n] = 0;
fprintf(stdout, "%s\n", buf); 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_shutdown(ssl);
SSL_free(ssl); SSL_free(ssl);
clixon_event_unreg_fd(s, tls_input_cb); clixon_event_unreg_fd(s, tls_server_reply_cb);
close(s); close(s);
free(sd); free(sd);
if (_connects == 1) if (_connects == 1)
@ -179,6 +230,8 @@ tls_input_cb(int s,
_connects--; _connects--;
retval = 0; retval = 0;
done: done:
if (expbuf)
free(expbuf);
clicon_debug(1, "%s %d", __FUNCTION__, retval); clicon_debug(1, "%s %d", __FUNCTION__, retval);
return retval; return retval;
} }
@ -260,30 +313,15 @@ tls_write_file(FILE *fp,
{ {
int retval = -1; int retval = -1;
char *buf = NULL; char *buf = NULL;
int buflen = 1024; /* start size */ size_t len = 0;
char ch;
int ret; int ret;
int sslerr; int sslerr;
size_t len = 0;
if ((buf = malloc(buflen)) == NULL){ if (read_data_file(fp, &buf, &len) < 0)
clicon_err(OE_UNIX, errno, "malloc");
goto done; goto done;
}
memset(buf, 0, buflen);
while (1){
if ((ret = fread(&ch, 1, 1, fp)) < 0){
clicon_err(OE_JSON, errno, "read");
goto done;
}
if (ret == 0)
break;
buf[len++] = ch;
// XXX No realloc, can overflow
}
if ((ret = SSL_write(ssl, buf, len)) < 1){ if ((ret = SSL_write(ssl, buf, len)) < 1){
sslerr = SSL_get_error(ssl, ret); sslerr = SSL_get_error(ssl, ret);
clicon_debug(1, "%s SSL_read() n:%d errno:%d sslerr:%d", __FUNCTION__, ret, errno, sslerr); clicon_debug(1, "%s SSL_write() n:%d errno:%d sslerr:%d", __FUNCTION__, ret, errno, sslerr);
} }
retval = 0; retval = 0;
done: done:
@ -292,11 +330,11 @@ tls_write_file(FILE *fp,
return retval; return retval;
} }
/*! Callhome-server accept socket /*! Callhome-server accept socket
*/ */
static int static int
tls_accept_cb(int ss, tls_server_accept_cb(int ss,
void *arg) void *arg)
{ {
int retval = -1; int retval = -1;
tls_accept_handle *ta = (tls_accept_handle *)arg; tls_accept_handle *ta = (tls_accept_handle *)arg;
@ -327,7 +365,7 @@ tls_accept_cb(int ss,
if (tls_write_file(ta->ta_f, ssl) < 0) if (tls_write_file(ta->ta_f, ssl) < 0)
goto done; goto done;
/* register callback for reply */ /* register callback for reply */
if (clixon_event_reg_fd(s, tls_input_cb, sd, "tls data") < 0) if (clixon_event_reg_fd(s, tls_server_reply_cb, sd, "tls server reply") < 0)
goto done; goto done;
retval = 0; retval = 0;
done: done:
@ -400,7 +438,8 @@ usage(char *argv0)
"where options are\n" "where options are\n"
"\t-h \t\tHelp\n" "\t-h \t\tHelp\n"
"\t-D <level> \tDebug\n" "\t-D <level> \tDebug\n"
"\t-f <file> \tHHTP input file (overrides stdin)\n" "\t-f <file> \tHTTP input file (overrides stdin)\n"
"\t-e <file> \tFile including HTTP output (otherwise echo to stdout)\n"
"\t-F ipv4|ipv6 \tSocket address family(ipv4 default)\n" "\t-F ipv4|ipv6 \tSocket address family(ipv4 default)\n"
"\t-a <addrstr> \tIP address (eg 1.2.3.4) - mandatory\n" "\t-a <addrstr> \tIP address (eg 1.2.3.4) - mandatory\n"
"\t-p <port> \tPort (default %d)\n" "\t-p <port> \tPort (default %d)\n"
@ -528,14 +567,14 @@ main(int argc,
ta->ta_ctx = ctx; ta->ta_ctx = ctx;
ta->ta_ss = ss; ta->ta_ss = ss;
ta->ta_f = fp; ta->ta_f = fp;
if (clixon_event_reg_fd(ss, tls_accept_cb, ta, "tls accept socket") < 0) if (clixon_event_reg_fd(ss, tls_server_accept_cb, ta, "tls server accept") < 0)
goto done; goto done;
if (clixon_event_loop(h) < 0) if (clixon_event_loop(h) < 0)
goto done; goto done;
retval = 0; retval = 0;
done: done:
if (ss != -1) if (ss != -1)
clixon_event_unreg_fd(ss, tls_accept_cb); clixon_event_unreg_fd(ss, tls_server_accept_cb);
if (ta) if (ta)
free(ta); free(ta);
if (fp) if (fp)

View file

@ -258,6 +258,9 @@ module clixon-restconf {
On platforms where namespaces are not suppported, 'default' On platforms where namespaces are not suppported, 'default'
Default value can be changed by RESTCONF_NETNS_DEFAULT"; Default value can be changed by RESTCONF_NETNS_DEFAULT";
} }
leaf description{
type string;
}
leaf address { leaf address {
type inet:ip-address; type inet:ip-address;
description "IP address to bind to"; description "IP address to bind to";
@ -299,28 +302,28 @@ module clixon-restconf {
presence presence
"Indicates periodic connects"; "Indicates periodic connects";
leaf period { leaf period {
type uint16; type uint32; /* XXX: note uit16 in std */
units "minutes"; units "seconds"; /* XXX: note minutes in standard */
default "60"; default "60";
description description
"Duration of time between periodic connections."; "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.";
}
} }
} }
} }
} }
container reconnect-strategy {
leaf max-attempts {
type uint8 {
range "1..max";
}
default "3";
description
"Specifies the number times the RESTCONF server tries
to connect to a specific endpoint before moving on to
the next endpoint in the list (round robin).";
}
}
} }
} }
} }

View file

@ -1,101 +0,0 @@
module ietf-list-pagination-nc {
yang-version 1.1;
namespace "urn:ietf:params:xml:ns:yang:ietf-list-pagination-nc";
prefix lpgnc;
import ietf-netconf {
prefix nc;
reference
"RFC 6241: Network Configuration Protocol (NETCONF)";
}
import ietf-netconf-nmda {
prefix ncds;
reference
"RFC 8526: NETCONF Extensions to Support the
Network Management Datastore Architecture";
}
import ietf-list-pagination {
prefix lp;
reference
"RFC XXXX: List Pagination for YANG-driven Protocols";
}
organization
"IETF NETCONF (Network Configuration) Working Group";
contact
"WG Web: <http://tools.ietf.org/wg/netconf/>
WG List: <mailto:netconf@ietf.org>";
description
"This module augments the <get>, <get-config>, and <get-data>
'rpc' statements to support list pagination.
Copyright (c) 2021 IETF Trust and the persons identified
as authors of the code. All rights reserved.
Redistribution and use in source and binary forms, with
or without modification, is permitted pursuant to, and
subject to the license terms contained in, the Simplified
BSD License set forth in Section 4.c of the IETF Trust's
Legal Provisions Relating to IETF Documents
(https://trustee.ietf.org/license-info).
This version of this YANG module is part of RFC XXXX
(https://www.rfc-editor.org/info/rfcXXXX); see the RFC
itself for full legal notices.
The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL',
'SHALL NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED',
'NOT RECOMMENDED', 'MAY', and 'OPTIONAL' in this document
are to be interpreted as described in BCP 14 (RFC 2119)
(RFC 8174) when, and only when, they appear in all
capitals, as shown here.";
revision 2021-10-25 {
description
"Initial revision.";
reference
"RFC XXXX: NETCONF Extensions to Support List Pagination";
}
grouping pagination-parameters {
description "A grouping for list pagination parameters.";
container list-pagination {
description "List pagination parameters.";
presence "Flag that request contains pagination parameters";
uses lp:where-param-grouping;
uses lp:sort-by-param-grouping;
uses lp:direction-param-grouping;
uses lp:offset-param-grouping;
uses lp:limit-param-grouping;
uses lp:sublist-limit-param-grouping;
}
}
augment "/nc:get/nc:input" {
description
"Allow the 'get' operation to use content filter
parameter for specifying the YANG list or leaf-list
that is to be retrieved";
uses pagination-parameters;
}
augment "/nc:get-config/nc:input" {
description
"Allow the 'get-config' operation to use content filter
parameter for specifying the YANG list or leaf-list
that is to be retrieved";
uses pagination-parameters;
}
augment "/ncds:get-data/ncds:input" {
description
"Allow the 'get-data' operation to use content filter
parameter for specifying the YANG list or leaf-list
that is to be retrieved";
uses pagination-parameters;
}
}

View file

@ -1,299 +0,0 @@
module ietf-list-pagination {
yang-version 1.1;
namespace
"urn:ietf:params:xml:ns:yang:ietf-list-pagination";
prefix lpg;
import ietf-yang-types {
prefix yang;
reference
"RFC 6991: Common YANG Data Types";
}
import ietf-yang-metadata {
prefix md;
reference
"RFC 7952: Defining and Using Metadata with YANG";
}
/* XXX system-capabilities brings in NACM that breaks clixon testing
import ietf-system-capabilities {
prefix sysc;
reference
"draft-ietf-netconf-notification-capabilities:
YANG Modules describing Capabilities for
Systems and Datastore Update Notifications";
}
*/
organization
"IETF NETCONF (Network Configuration) Working Group";
contact
"WG Web: <http://tools.ietf.org/wg/netconf/>
WG List: <mailto:netconf@ietf.org>";
description
"This module is used by servers to 1) indicate they support
pagination on 'list' and 'leaf-list' resources, 2) define a
grouping for each list-pagination parameter, and 3) indicate
which 'config false' lists have constrained 'where' and
'sort-by' parameters and how they may be used, if at all.
Copyright (c) 2021 IETF Trust and the persons identified
as authors of the code. All rights reserved.
Redistribution and use in source and binary forms, with
or without modification, is permitted pursuant to, and
subject to the license terms contained in, the Simplified
BSD License set forth in Section 4.c of the IETF Trust's
Legal Provisions Relating to IETF Documents
(https://trustee.ietf.org/license-info).
This version of this YANG module is part of RFC XXXX
(https://www.rfc-editor.org/info/rfcXXXX); see the RFC
itself for full legal notices.
The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL',
'SHALL NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED',
'NOT RECOMMENDED', 'MAY', and 'OPTIONAL' in this document
are to be interpreted as described in BCP 14 (RFC 2119)
(RFC 8174) when, and only when, they appear in all
capitals, as shown here.";
revision 2021-10-25 {
description
"Initial revision.";
reference
"RFC XXXX: List Pagination for YANG-driven Protocols";
}
// Annotations
md:annotation remaining {
type union {
type uint32;
type enumeration {
enum "unknown" {
description
"Indicates that number of remaining entries is unknown
to the server in case, e.g., the server has determined
that counting would be prohibitively expensive.";
}
}
}
description
"This annotation contains the number of elements not included
in the result set (a positive value) due to a 'limit' or
'sublist-limit' operation. If no elements were removed,
this annotation MUST NOT appear. The minimum value (0),
which never occurs in normal operation, is reserved to
represent 'unknown'. The maximum value (2^32-1) is
reserved to represent any value greater than or equal
to 2^32-1 elements.";
}
// Identities
identity list-pagination-error {
description
"Base identity for list-pagination errors.";
}
identity offset-out-of-range {
base list-pagination-error;
description
"The 'offset' query parameter value is greater than the number
of instances in the target list or leaf-list resource.";
}
// Groupings
grouping where-param-grouping {
description
"This grouping may be used by protocol-specific YANG modules
to define a protocol-specific query parameter.";
leaf where {
type union {
type yang:xpath1.0;
type enumeration {
enum "unfiltered" {
description
"Indicates that no entries are to be filtered
from the working result-set.";
}
}
}
default "unfiltered";
description
"The 'where' parameter specifies a boolean expression
that result-set entries must match.
It is an error if the XPath expression references a node
identifier that does not exist in the schema, is optional
or conditional in the schema or, for constrained 'config
false' lists and leaf-lists, if the node identifier does
not point to a node having the 'indexed' extension
statement applied to it (see RFC XXXX).";
}
}
grouping sort-by-param-grouping {
description
"This grouping may be used by protocol-specific YANG modules
to define a protocol-specific query parameter.";
leaf sort-by {
type union {
type string {
// An RFC 7950 'descendant-schema-nodeid'.
pattern '([0-9a-fA-F]*:)?[0-9a-fA-F]*'
+ '(/([0-9a-fA-F]*:)?[0-9a-fA-F]*)*';
}
type enumeration {
enum "none" {
description
"Indicates that the list or leaf-list's default
order is to be used, per the YANG 'ordered-by'
statement.";
}
}
}
default "none";
description
"The 'sort-by' parameter indicates the node in the
working result-set (i.e., after the 'where' parameter
has been applied) that entries should be sorted by.
Sorts are in ascending order (e.g., '1' before '9',
'a' before 'z', etc.). Missing values are sorted to
the end (e.g., after all nodes having values).";
}
}
grouping direction-param-grouping {
description
"This grouping may be used by protocol-specific YANG modules
to define a protocol-specific query parameter.";
leaf direction {
type enumeration {
enum forwards {
description
"Indicates that entries should be traversed from
the first to last item in the working result set.";
}
enum backwards {
description
"Indicates that entries should be traversed from
the last to first item in the working result set.";
}
}
default "forwards";
description
"The 'direction' parameter indicates how the entries in the
working result-set (i.e., after the 'sort-by' parameter
has been applied) should be traversed.";
}
}
grouping offset-param-grouping {
description
"This grouping may be used by protocol-specific YANG modules
to define a protocol-specific query parameter.";
leaf offset {
type uint32;
default 0;
description
"The 'offset' parameter indicates the number of entries
in the working result-set (i.e., after the 'direction'
parameter has been applied) that should be skipped over
when preparing the response.";
}
}
grouping limit-param-grouping {
description
"This grouping may be used by protocol-specific YANG modules
to define a protocol-specific query parameter.";
leaf limit {
type union {
type uint32 {
range "1..max";
}
type enumeration {
enum "unbounded" {
description
"Indicates that the number of entries that may be
returned is unbounded.";
}
}
}
default "unbounded";
description
"The 'limit' parameter limits the number of entries returned
from the working result-set (i.e., after the 'offset'
parameter has been applied).
Any result-set that is limited includes, somewhere in its
encoding, the metadata value 'remaining' to indicate the
number entries not included in the result set.";
}
}
grouping sublist-limit-param-grouping {
description
"This grouping may be used by protocol-specific YANG modules
to define a protocol-specific query parameter.";
leaf sublist-limit {
type union {
type uint32 {
range "1..max";
}
type enumeration {
enum "unbounded" {
description
"Indicates that the number of entries that may be
returned is unbounded.";
}
}
}
default "unbounded";
description
"The 'sublist-limit' parameter limits the number of entries
for descendent lists and leaf-lists.
Any result-set that is limited includes, somewhere in
its encoding, the metadata value 'remaining' to indicate
the number entries not included in the result set.";
}
}
// Protocol-accessible nodes
/* XXX system-capabilities brings in NACM that breaks clixon testing
augment // FIXME: ensure datastore == <operational>
"/sysc:system-capabilities/sysc:datastore-capabilities"
+ "/sysc:per-node-capabilities" {
description
"Defines some leafs that MAY be used by the server to
describe constraints imposed of the 'where' filters and
'sort-by' parameters used in list pagination queries.";
leaf constrained {
type empty;
description
"Indicates that 'where' filters and 'sort-by' parameters
on the targeted 'config false' list node are constrained.
If a list is not 'constrained', then full XPath 1.0
expressions may be used in 'where' filters and all node
identifiers are usable by 'sort-by'.";
}
leaf indexed {
type empty;
description
"Indicates that the targeted descendent node of a
'constrained' list (see the 'constrained' leaf) may be
used in 'where' filters and/or 'sort-by' parameters.
If a descendent node of a 'constrained' list is not
'indexed', then it MUST NOT be used in 'where' filters
or 'sort-by' parameters.";
}
}
*/
}