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] 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 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 *callhome,
int *periodic,
uint32_t *period,
uint8_t *attempts)
{
int retval = -1;
cxobj *x;
@ -900,6 +909,9 @@ restconf_socket_extract(clicon_handle h,
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;
@ -963,8 +975,37 @@ restconf_socket_extract(clicon_handle h,
goto done;
}
}
if ((x = xpath_first(xs, nsc, "call-home")) != NULL)
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;

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_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 **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_ */

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);
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;
retval = 0;
done:
@ -548,342 +548,6 @@ restconf_checkcert_file(cxobj *xrestconf,
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
* @param[in] fd Socket (unix or ip)
* @param[in] arg typecast clicon_handle
@ -939,11 +603,16 @@ restconf_native_terminate(clicon_handle h)
clicon_debug(1, "%s", __FUNCTION__);
if ((rh = restconf_native_handle_get(h)) != 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);
close(rsock->rs_ss);
}
DELQ(rsock, rh->rh_sockets, restconf_socket *);
if (rsock->rs_description)
free(rsock->rs_description);
if (rsock->rs_addrstr)
free(rsock->rs_addrstr);
if (rsock->rs_addrtype)
@ -1028,62 +697,6 @@ restconf_clixon_backend(clicon_handle h,
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
* @param[in] h Clicon handle
* @param[in] xs XML config of single restconf socket
@ -1097,6 +710,7 @@ openssl_init_socket(clicon_handle h,
cvec *nsc)
{
int retval = -1;
char *description = NULL;
char *netns = NULL;
char *address = NULL;
char *addrtype = NULL;
@ -1106,10 +720,14 @@ openssl_init_socket(clicon_handle h,
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, &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;
/*
@ -1124,6 +742,16 @@ openssl_init_socket(clicon_handle h,
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");
goto done;
}
if (callhome){
if (!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);
if (callhome){
rsock->rs_ss = -1; /* Not applicable fro callhome */
if (restconf_callhome_timer(0, rsock) < 0)
rsock->rs_ss = -1; /* Not applicable from callhome */
if (restconf_callhome_timer(rsock, 0) < 0)
goto done;
}
else {

View file

@ -168,7 +168,8 @@ restconf_stream_free(restconf_stream_data *sd)
*/
restconf_conn *
restconf_conn_new(clicon_handle h,
int s)
int s,
restconf_socket *rsock)
{
restconf_conn *rc;
@ -179,6 +180,7 @@ restconf_conn_new(clicon_handle h,
memset(rc, 0, sizeof(restconf_conn));
rc->rc_h = h;
rc->rc_s = s;
rc->rc_socket = rsock;
return rc;
}
@ -341,7 +343,8 @@ native_buf_write(clicon_handle h,
char *buf,
size_t buflen,
int s,
SSL *ssl)
SSL *ssl,
restconf_socket *rsock)
{
int retval = -1;
ssize_t len;
@ -376,7 +379,7 @@ native_buf_write(clicon_handle h,
if (ssl){
SSL_free(ssl);
}
if (restconf_connection_close(h, s) < 0)
if (restconf_connection_close(h, s, rsock) < 0)
goto done;
goto ok; /* Close socket and ssl */
}
@ -408,7 +411,7 @@ native_buf_write(clicon_handle h,
break;
case ECONNRESET: /* Connection reset by peer */
case EPIPE: /* Broken pipe */
if (restconf_connection_close(h, s) < 0)
if (restconf_connection_close(h, s, rsock) < 0)
goto done;
goto ok; /* Close socket and ssl */
break;
@ -436,12 +439,13 @@ native_buf_write(clicon_handle h,
* @param[in] body If given add message body using media
* @see restconf_badrequest which can only be called in a request context
*/
int
static int
native_send_badrequest(clicon_handle h,
int s,
SSL *ssl,
char *media,
char *body)
char *body,
restconf_socket *rsock)
{
int retval = -1;
cbuf *cb = NULL;
@ -461,7 +465,7 @@ native_send_badrequest(clicon_handle h,
cprintf(cb, "\r\n");
if (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;
retval = 0;
done:
@ -574,7 +578,7 @@ read_regular(restconf_conn *rc,
switch(errno){
case ECONNRESET:/* Connection reset by peer */
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;
restconf_conn_free(rc);
retval = 0; /* Close socket and ssl */
@ -675,7 +679,7 @@ restconf_http1_process(restconf_conn *rc,
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);
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 ok;
}
@ -686,7 +690,7 @@ restconf_http1_process(restconf_conn *rc,
goto done;
if (ret == 1){
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;
cvec_reset(sd->sd_outp_hdrs);
cbuf_reset(sd->sd_outp_buf);
@ -714,7 +718,7 @@ restconf_http1_process(restconf_conn *rc,
if (restconf_http1_path_root(h, rc) < 0)
goto done;
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;
cvec_reset(sd->sd_outp_hdrs); /* Can be done in native_send_reply */
cbuf_reset(sd->sd_outp_buf);
@ -723,7 +727,7 @@ restconf_http1_process(restconf_conn *rc,
if (rc->rc_exit){ /* Server-initiated exit */
SSL_free(rc->rc_ssl);
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;
restconf_conn_free(rc);
retval = 0;
@ -869,6 +873,8 @@ restconf_native_handle_get(clicon_handle h)
return NULL;
}
/*---------------------------- Connect ---------------------------------*/
/*! New data connection after accept, receive and reply on data socket
*
* @param[in] s Socket where message arrived. read from this.
@ -959,37 +965,491 @@ restconf_connection(int s,
return retval;
} /* restconf_connection */
int restconf_callhome_timer(int fd, void *arg); // XXX FIX HEADERS INSTEAD
/*! Close Restconf native connection socket and unregister callback
* For callhome also start reconnect timer
*/
int
restconf_connection_close(clicon_handle h,
int s)
int s,
restconf_socket *rsock)
{
int retval = -1;
restconf_native_handle *rh = NULL;
restconf_socket *rsock;
clicon_debug(1, "%s", __FUNCTION__);
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;
retval = 0;
done:
clicon_debug(1, "%s %d", __FUNCTION__, retval);
return retval;
}
if (0 && (rh = restconf_native_handle_get(h)) != NULL){
while ((rsock = rh->rh_sockets) != NULL)
if (rsock->rs_callhome && s == rsock->rs_ss){
/* Maybe we should have a special wrapper function that only sets timer here? */
if (restconf_callhome_timer(0, rsock) < 0)
/*------------------------------ Accept--------------------------------*/
/*! 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), 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;
done:
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,6 +85,37 @@ typedef struct {
uint8_t *sd_settings2; /* Settings for upgrade to http/2 request */
} restconf_stream_data;
/* Restconf per socket handle
* Two types: listen and callhome.
* Listen: Uses socket rs_ss to listen for connections and accepts them, creates one
* restconf_conn for each new accept.
* Callhome: Calls connect according to timer to setup single restconf_conn.
* when this is closed, new connect is made, according to connection-type.
*/
typedef struct {
qelem_t rs_qelem; /* List header */
clicon_handle rs_h; /* Clixon handle */
char *rs_description; /* Description */
int rs_callhome; /* 0: listen, 1: callhome */
int rs_ss; /* Listen: Server socket, ready for accept
* Callhome: connect socket (same as restconf_conn->rc_s) */
int rs_ssl; /* 0: Not SSL socket, 1:SSL socket */
char *rs_addrtype; /* Address type according to ietf-inet-types:
eg inet:ipv4-address or inet:ipv6-address */
char *rs_addrstr; /* Address as string, eg 127.0.0.1, ::1 */
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 connection handle
* Per connection request
*/
@ -105,27 +136,9 @@ typedef struct restconf_conn {
#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 per socket handle
* Two types: listen and callhome.
* Listen: Uses socket rs_ss to listen for connections and accepts them, creates one
* restconf_conn for each new accept.
* Callhome: Calls connect according to timer to setup single restconf_conn.
* when this is closed, new connect is made, according to connection-type.
*/
typedef struct {
qelem_t rs_qelem; /* List header */
clicon_handle rs_h; /* Clixon handle */
int rs_callhome; /* 0: listen, 1: callhome */
int rs_ss; /* Listen: Server socket, ready for accept
* Callhome: connect socket (same as restconf_conn->rc_s) */
int rs_ssl; /* 0: Not SSL socket, 1:SSL socket */
char *rs_addrtype; /* Address type according to ietf-inet-types:
eg inet:ipv4-address or inet:ipv6-address */
char *rs_addrstr; /* Address as string, eg 127.0.0.1, ::1 */
uint16_t rs_port; /* Protocol port */
} restconf_socket;
/* Restconf handle
* 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_find(restconf_conn *rc, int32_t id);
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 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_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);
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_ */

View file

@ -2,6 +2,13 @@
# test of Restconf callhome
# See RFC 8071 NETCONF Call Home and RESTCONF Call Home
# 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)
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
clispec=$dir/spec.cli
# HTTP request client->server
frequest=$dir/frequest
# HTTP expected reply server->client
certdir=$dir/certs
cakey=$certdir/ca_key.pem
cacert=$certdir/ca_cert.pem
@ -60,17 +71,39 @@ cat <<EOF > $cfg
<server-ca-cert-path>$cacert</server-ca-cert-path>
<debug>1</debug>
<socket>
<description>callhome persistent</description>
<namespace>default</namespace>
<call-home>
<connection-type>
<persistent/>
</connection-type>
<reconnect-strategy>
<max-attempts>3</max-attempts>
</reconnect-strategy>
</call-home>
<address>127.0.0.1</address>
<port>4336</port>
<ssl>true</ssl>
</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>
<address>0.0.0.0</address>
<port>443</port>
@ -219,14 +252,16 @@ cat <<EOF > $dir/startup_db
</${DATASTORE_TOP}>
EOF
# Callhome request from client
cat <<EOF > $dir/data
# Callhome request from client->server
cat <<EOF > $frequest
GET /restconf/data/clixon-example:table HTTP/$HVERCH
Host: localhost
Accept: application/yang-data+xml
EOF
expectreply="<table xmlns=\"urn:example:clixon\"><parameter><name>x</name><value>foo</value></parameter></table>"
new "test params: -f $cfg"
# Bring your own backend
if [ $BE -ne 0 ]; then
@ -249,7 +284,7 @@ if [ $RC -ne 0 ]; then
stop_restconf_pre
new "start restconf daemon"
start_restconf -f $cfg
start_restconf -f $cfg -D 1 -l s
fi
new "wait restconf"
@ -258,9 +293,39 @@ 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"
new "Send GET via callhome client"
echo "${clixon_restconf_callhome_client} -D $DBG -f $dir/data -a 127.0.0.1 -c $srvcert -k $srvkey -C $cacert"
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"
# 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"
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
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
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
@ -66,7 +68,7 @@
/* clixon */
#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
@ -143,10 +145,50 @@ callhome_bind(struct sockaddr *sa,
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
tls_input_cb(int s,
read_data_file(FILE *fe,
char **bufp,
size_t *lenp)
{
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;
@ -154,6 +196,11 @@ tls_input_cb(int s,
SSL *ssl;
char buf[1024];
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__);
ssl = sd->sd_ssl;
@ -167,10 +214,14 @@ tls_input_cb(int s,
goto done;
}
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_free(ssl);
clixon_event_unreg_fd(s, tls_input_cb);
clixon_event_unreg_fd(s, tls_server_reply_cb);
close(s);
free(sd);
if (_connects == 1)
@ -179,6 +230,8 @@ tls_input_cb(int s,
_connects--;
retval = 0;
done:
if (expbuf)
free(expbuf);
clicon_debug(1, "%s %d", __FUNCTION__, retval);
return retval;
}
@ -260,30 +313,15 @@ tls_write_file(FILE *fp,
{
int retval = -1;
char *buf = NULL;
int buflen = 1024; /* start size */
char ch;
size_t len = 0;
int ret;
int sslerr;
size_t len = 0;
if ((buf = malloc(buflen)) == NULL){
clicon_err(OE_UNIX, errno, "malloc");
if (read_data_file(fp, &buf, &len) < 0)
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){
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;
done:
@ -295,7 +333,7 @@ tls_write_file(FILE *fp,
/*! Callhome-server accept socket
*/
static int
tls_accept_cb(int ss,
tls_server_accept_cb(int ss,
void *arg)
{
int retval = -1;
@ -327,7 +365,7 @@ tls_accept_cb(int ss,
if (tls_write_file(ta->ta_f, ssl) < 0)
goto done;
/* 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;
retval = 0;
done:
@ -400,7 +438,8 @@ usage(char *argv0)
"where options are\n"
"\t-h \t\tHelp\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-a <addrstr> \tIP address (eg 1.2.3.4) - mandatory\n"
"\t-p <port> \tPort (default %d)\n"
@ -528,14 +567,14 @@ main(int argc,
ta->ta_ctx = ctx;
ta->ta_ss = ss;
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;
if (clixon_event_loop(h) < 0)
goto done;
retval = 0;
done:
if (ss != -1)
clixon_event_unreg_fd(ss, tls_accept_cb);
clixon_event_unreg_fd(ss, tls_server_accept_cb);
if (ta)
free(ta);
if (fp)

View file

@ -258,6 +258,9 @@ module clixon-restconf {
On platforms where namespaces are not suppported, 'default'
Default value can be changed by RESTCONF_NETNS_DEFAULT";
}
leaf description{
type string;
}
leaf address {
type inet:ip-address;
description "IP address to bind to";
@ -299,26 +302,26 @@ module clixon-restconf {
presence
"Indicates periodic connects";
leaf period {
type uint16;
units "minutes";
type uint32; /* XXX: note uit16 in std */
units "seconds"; /* XXX: note minutes in standard */
default "60";
description
"Duration of time between periodic connections.";
}
leaf idle-timeout {
type uint16;
units "seconds";
default "120"; // two minutes
}
}
}
}
container reconnect-strategy {
leaf max-attempts {
type uint8 {
range "1..max";
}
default "3";
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.";
}
}
}
"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.";
}
}
*/
}