diff --git a/CHANGELOG.md b/CHANGELOG.md index 45a42994..4bd7ee48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ Expected: September 2022 ### New features * RESTCONF call home according to RFC 8071 + * Experimental, work-in-progress ### Corrected Bugs @@ -102,7 +103,7 @@ Users may have to change how they access the system Developers may need to change their code * Changed C-API for xml translation/print the internal `cxobj` tree data structure to other formats. - * Functions are merged, ie removed and replaced with more generic functions + * Functions are merged, ie removed and with replaced more generic functions * Added `skiptop` parameter, if set only apply to children of a node, skip top node * default is 0 * The new API is as follows: diff --git a/apps/restconf/restconf_main_native.c b/apps/restconf/restconf_main_native.c index 5118b976..afa6f54c 100644 --- a/apps/restconf/restconf_main_native.c +++ b/apps/restconf/restconf_main_native.c @@ -171,21 +171,6 @@ static int session_id_context = 1; -/*! Get restconf native handle - * @param[in] h Clicon handle - * @retval rh Restconf native handle - */ -static restconf_native_handle * -restconf_native_handle_get(clicon_handle h) -{ - clicon_hash_t *cdat = clicon_data(h); - size_t len; - void *p; - - if ((p = clicon_hash_value(cdat, "restconf-native-handle", &len)) != NULL) - return *(restconf_native_handle **)p; - return NULL; -} /*! Set restconf native handle * @param[in] h Clicon handle @@ -489,11 +474,8 @@ Note that in this case SSL_ERROR_ZERO_RETURN does not necessarily indicate that SSL_free(rc->rc_ssl); rc->rc_ssl = NULL; } - if (close(rc->rc_s) < 0){ - clicon_err(OE_UNIX, errno, "close"); + if (restconf_connection_close(rc->rc_h, rc->rc_s) < 0) goto done; - } - clixon_event_unreg_fd(rc->rc_s, restconf_connection); retval = 0; done: clicon_debug(1, "%s retval:%d", __FUNCTION__, retval); @@ -643,9 +625,10 @@ ssl_alpn_check(clicon_handle h, } /* ssl_alpn_check */ -/*! Accept new socket client (ssl not ip) - * @param[in] fd Socket (unix or ip) - * @param[in] arg typecast clicon_handle +/*! 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 @@ -671,12 +654,10 @@ restconf_ssl_accept_client(clicon_handle h, proto = HTTP_2; /* If nghttp2 only let default be 2.0 */ #endif #endif -#if 1 if ((rh = restconf_native_handle_get(h)) == NULL){ clicon_err(OE_XML, EFAULT, "No openssl handle"); goto done; } -#endif /* * Register callbacks for actual data socket */ @@ -737,11 +718,8 @@ restconf_ssl_accept_client(clicon_handle h, #endif SSL_free(rc->rc_ssl); rc->rc_ssl = NULL; - clixon_event_unreg_fd(rc->rc_s, restconf_connection); - if (close(rc->rc_s) < 0){ - clicon_err(OE_UNIX, errno, "close"); + if (restconf_connection_close(h, rc->rc_s) < 0) goto done; - } restconf_conn_free(rc); goto ok; break; @@ -961,8 +939,10 @@ 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){ - clixon_event_unreg_fd(rsock->rs_ss, restconf_accept_client); - close(rsock->rs_ss); + 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_addrstr) free(rsock->rs_addrstr); @@ -1049,9 +1029,12 @@ restconf_clixon_backend(clicon_handle h, } /*! Periodically try to connect to callhome client + * + * @param[in] fd No-op + * @param[in] arg restconf_socket */ int -restconf_callhome_timer(int fd, +restconf_callhome_timer(int fdxxx, void *arg) { int retval = -1; @@ -1059,7 +1042,7 @@ restconf_callhome_timer(int fd, struct timeval now; struct timeval t; struct timeval t1 = {1, 0}; // XXX once every second - restconf_socket *rs; + 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; @@ -1067,12 +1050,13 @@ restconf_callhome_timer(int fd, clicon_debug(1, "%s", __FUNCTION__); gettimeofday(&now, NULL); - if ((rs = (restconf_socket *)arg) == NULL){ + if ((rsock = (restconf_socket *)arg) == NULL){ clicon_err(OE_YANG, EINVAL, "rsock is NULL"); goto done; } - h = rs->rs_h; - if (clixon_inet2sin(rs->rs_addrtype, rs->rs_addrstr, rs->rs_port, sa, &sa_len) < 0) + 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"); @@ -1080,18 +1064,18 @@ restconf_callhome_timer(int fd, } 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 */ - rs, + rsock, "restconf callhome timer") < 0) goto done; } else { - rs->rs_ss = s; - if (restconf_ssl_accept_client(h, rs->rs_ss, rs) < 0) + if (restconf_ssl_accept_client(h, s, rsock) < 0) goto done; } clicon_debug(1, "%s connect done", __FUNCTION__); @@ -1139,6 +1123,7 @@ openssl_init_socket(clicon_handle h, memset(rsock, 0, sizeof *rsock); rsock->rs_h = h; rsock->rs_ssl = ssl; /* true/false */ + rsock->rs_callhome = callhome; if (callhome){ if (!ssl){ clicon_err(OE_SSL, EINVAL, "Restconf callhome requires SSL"); @@ -1162,9 +1147,6 @@ openssl_init_socket(clicon_handle h, clicon_err(OE_XML, EFAULT, "No openssl handle"); goto done; } - - rsock->rs_ss = ss; - if ((rsock->rs_addrstr = strdup(address)) == NULL){ clicon_err(OE_UNIX, errno, "strdup"); goto done; @@ -1177,13 +1159,15 @@ openssl_init_socket(clicon_handle h, INSQ(rsock, rh->rh_sockets); if (callhome){ - if (restconf_callhome_timer(ss, rsock) < 0) + rsock->rs_ss = -1; /* Not applicable fro callhome */ + if (restconf_callhome_timer(0, rsock) < 0) goto done; } else { /* ss is a server socket that the clients connect to. The callback therefore accepts clients on ss */ - if (clixon_event_reg_fd(ss, restconf_accept_client, rsock, "restconf socket") < 0) + rsock->rs_ss = ss; + if (clixon_event_reg_fd(rsock->rs_ss, restconf_accept_client, rsock, "restconf socket") < 0) goto done; } retval = 0; diff --git a/apps/restconf/restconf_native.c b/apps/restconf/restconf_native.c index 0377dfee..7a5b2d00 100644 --- a/apps/restconf/restconf_native.c +++ b/apps/restconf/restconf_native.c @@ -135,7 +135,6 @@ restconf_stream_find(restconf_conn *rc, return NULL; } - /* * @param[in] sd Restconf data stream */ @@ -338,10 +337,11 @@ restconf_connection_sanity(clicon_handle h, * see also this function in restcont_api_openssl.c */ static int -native_buf_write(char *buf, - size_t buflen, - int s, - SSL *ssl) +native_buf_write(clicon_handle h, + char *buf, + size_t buflen, + int s, + SSL *ssl) { int retval = -1; ssize_t len; @@ -376,8 +376,8 @@ native_buf_write(char *buf, if (ssl){ SSL_free(ssl); } - close(s); - clixon_event_unreg_fd(s, restconf_connection); + if (restconf_connection_close(h, s) < 0) + goto done; goto ok; /* Close socket and ssl */ } else if (er == EAGAIN){ @@ -408,8 +408,8 @@ native_buf_write(char *buf, break; case ECONNRESET: /* Connection reset by peer */ case EPIPE: /* Broken pipe */ - close(s); - clixon_event_unreg_fd(s, restconf_connection); + if (restconf_connection_close(h, s) < 0) + goto done; goto ok; /* Close socket and ssl */ break; default: @@ -461,7 +461,7 @@ native_send_badrequest(clicon_handle h, cprintf(cb, "\r\n"); if (body) cprintf(cb, "%s\r\n", body); - if (native_buf_write(cbuf_get(cb), cbuf_len(cb), s, ssl) < 0) + if (native_buf_write(h, cbuf_get(cb), cbuf_len(cb), s, ssl) < 0) goto done; retval = 0; done: @@ -574,8 +574,8 @@ 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); - clixon_event_unreg_fd(rc->rc_s, restconf_connection); - close(rc->rc_s); + if (restconf_connection_close(rc->rc_h, rc->rc_s) < 0) + goto done; restconf_conn_free(rc); retval = 0; /* Close socket and ssl */ goto done; @@ -685,7 +685,7 @@ restconf_http1_process(restconf_conn *rc, if ((ret = http1_check_expect(h, rc, sd)) < 0) goto done; if (ret == 1){ - if (native_buf_write(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) goto done; cvec_reset(sd->sd_outp_hdrs); @@ -713,7 +713,7 @@ restconf_http1_process(restconf_conn *rc, /* main restconf processing */ if (restconf_http1_path_root(h, rc) < 0) goto done; - if (native_buf_write(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) goto done; cvec_reset(sd->sd_outp_hdrs); /* Can be done in native_send_reply */ @@ -723,11 +723,8 @@ restconf_http1_process(restconf_conn *rc, if (rc->rc_exit){ /* Server-initiated exit */ SSL_free(rc->rc_ssl); rc->rc_ssl = NULL; - if (close(rc->rc_s) < 0){ - clicon_err(OE_UNIX, errno, "close"); + if (restconf_connection_close(h, rc->rc_s) < 0) goto done; - } - clixon_event_unreg_fd(rc->rc_s, restconf_connection); restconf_conn_free(rc); retval = 0; goto done; @@ -856,6 +853,22 @@ restconf_http2_process(restconf_conn *rc, } #endif /* HAVE_LIBNGHTTP2 */ +/*! Get restconf native handle + * @param[in] h Clicon handle + * @retval rh Restconf native handle + */ +restconf_native_handle * +restconf_native_handle_get(clicon_handle h) +{ + clicon_hash_t *cdat = clicon_data(h); + size_t len; + void *p; + + if ((p = clicon_hash_value(cdat, "restconf-native-handle", &len)) != NULL) + return *(restconf_native_handle **)p; + return NULL; +} + /*! New data connection after accept, receive and reply on data socket * * @param[in] s Socket where message arrived. read from this. @@ -945,3 +958,38 @@ restconf_connection(int s, clicon_debug(1, "%s retval %d", __FUNCTION__, retval); 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 retval = -1; + restconf_native_handle *rh = NULL; + restconf_socket *rsock; + + if (close(s) < 0){ + clicon_err(OE_UNIX, errno, "close"); + goto done; + } + clixon_event_unreg_fd(s, restconf_connection); + + 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) + goto done; + break; + } + } + + retval = 0; + done: + return retval; +} + diff --git a/apps/restconf/restconf_native.h b/apps/restconf/restconf_native.h index c08863a2..df019a71 100644 --- a/apps/restconf/restconf_native.h +++ b/apps/restconf/restconf_native.h @@ -92,9 +92,9 @@ 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 */ + 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 */ @@ -108,11 +108,18 @@ typedef struct restconf_conn { } 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_ss; /* Server socket (ready for accept) */ + 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 */ @@ -142,7 +149,9 @@ 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); #endif /* _RESTCONF_NATIVE_H_ */ diff --git a/apps/restconf/restconf_nghttp2.c b/apps/restconf/restconf_nghttp2.c index 7265c1aa..297b9cb5 100644 --- a/apps/restconf/restconf_nghttp2.c +++ b/apps/restconf/restconf_nghttp2.c @@ -224,7 +224,10 @@ session_send_callback(nghttp2_session *session, #if 1 else if (errno == ECONNRESET) {/* Connection reset by peer */ close(s); - // XXX clixon_event_unreg_fd(s, restconf_connection); + // XXXUnclear why this is commented, maybe should call + // restconf_connection_close? + // clixon_event_unreg_fd(s, restconf_connection); + goto ok; /* Close socket and ssl */ } #endif diff --git a/test/lib.sh b/test/lib.sh index 932b6c1f..b82b8aaf 100755 --- a/test/lib.sh +++ b/test/lib.sh @@ -190,9 +190,6 @@ BUSER=clicon : ${clixon_snmp_pidfile:="/var/tmp/clixon_snmp.pid"} -# Temporary debug var, set to trigger remaining snmp errors -: ${snmp_debug:=false} - # Source the site-specific definitions for test script variables, if site.sh # exists. The variables defined in site.sh override any variables of the same # names in the environment in the current execution. diff --git a/test/test_restconf_callhome.sh b/test/test_restconf_callhome.sh index 6dce2257..34e2e29f 100755 --- a/test/test_restconf_callhome.sh +++ b/test/test_restconf_callhome.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # test of Restconf callhome # See RFC 8071 NETCONF Call Home and RESTCONF Call Home -# No NACM for now +# Simple NACM for single "andy" user # Magic line must be first in script (see README.md) s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi @@ -49,6 +49,7 @@ cat < $cfg false $dir/$APPNAME.sock /usr/local/var/$APPNAME/$APPNAME.pidfile + internal $dir true @@ -109,6 +110,9 @@ module clixon-example{ yang-version 1.1; namespace "urn:example:clixon"; prefix ex; + import ietf-netconf-acm { + prefix nacm; + } /* Generic config data */ container table{ list parameter{ @@ -124,6 +128,37 @@ module clixon-example{ } EOF +# NACM rules +# Two groups: admin allow all, guest allow nothing +RULES=$(cat < + false + permit + deny + deny + + + admin + andy + + + + admin-acl + admin + + permit-all + * + * + permit + + Allow the 'admin' group complete access to all operations and data. + + + + +EOF +) + cat < $clispec CLICON_MODE="example"; CLICON_PROMPT="%U@%H %W> "; @@ -177,15 +212,16 @@ EOF openssl x509 -req -extfile $dir/$name.cnf -days 7 -passin "pass:password" -in $certdir/$name.csr -CA $cacert -CAkey $cakey -CAcreateserial -out $certdir/$name.crt || err "Generate signing client cert" done -# hardcoded to andy +# Just NACM for now cat < $dir/startup_db <${DATASTORE_TOP}> + $RULES EOF # Callhome request from client cat < $dir/data -GET /restconf/data HTTP/$HVERCH +GET /restconf/data/clixon-example:table HTTP/$HVERCH Host: localhost Accept: application/yang-data+xml @@ -223,6 +259,7 @@ 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" # Kill old diff --git a/util/clixon_restconf_callhome_client.c b/util/clixon_restconf_callhome_client.c index a032273a..029cdc22 100644 --- a/util/clixon_restconf_callhome_client.c +++ b/util/clixon_restconf_callhome_client.c @@ -38,7 +38,9 @@ | clixon_restconf | ----------------> | callhome-client | <------ 3) HTTP | | 2) tls | | +-----------------+ <--------------- +-----------------+ - + + 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. */ #ifdef HAVE_CONFIG_H @@ -64,15 +66,15 @@ /* clixon */ #include "clixon/clixon.h" -#define UTIL_TLS_OPTS "hD:f:F:a:p:c:C:k:" +#define UTIL_TLS_OPTS "hD:f:F:a:p:c:C:k:n:" #define RESTCONF_CH_TLS 4336 /* User struct for context / accept */ typedef struct { - int ta_ss; /* accept socket */ - SSL_CTX *ta_ctx; /* SSL context */ - FILE *ta_f; /* Input data file */ + int ta_ss; /* Accept socket */ + SSL_CTX *ta_ctx; /* SSL context */ + FILE *ta_f; /* Input data file */ } tls_accept_handle; /* User connection-specific data handle */ @@ -81,6 +83,9 @@ typedef struct { SSL *sd_ssl; /* SSL connection data */ } tls_session_data; +/* Expected connects */ +static int _connects = 1; + /*! Create and bind stream socket * @param[in] sa Socketaddress * @param[in] sa_len Length of sa. Tecynicaliyu to be independent of sockaddr sa_len @@ -168,8 +173,13 @@ tls_input_cb(int s, clixon_event_unreg_fd(s, tls_input_cb); close(s); free(sd); - clixon_exit_set(1); /* XXX more elaborate logic: 1) continue request, 2) close and accept new */ + if (_connects == 1) + clixon_exit_set(1); /* XXX more elaborate logic: 1) continue request, 2) close and accept new */ + else + _connects--; + retval = 0; done: + clicon_debug(1, "%s %d", __FUNCTION__, retval); return retval; } @@ -383,8 +393,6 @@ tls_ctx_init(const char *cert_path, return NULL; } - - static int usage(char *argv0) { @@ -399,6 +407,7 @@ usage(char *argv0) "\t-c \tcert\n" "\t-C \tcacert\n" "\t-k \tkey\n" + "\t-n \tExpected incoming connections, 0 means no limit. Default: 1\n" , argv0, RESTCONF_CH_TLS); @@ -472,6 +481,11 @@ main(int argc, usage(argv[0]); key_path = optarg; break; + case 'n': + if (optarg == NULL || *optarg == '-') + usage(argv[0]); + _connects = atoi(optarg); + break; default: usage(argv[0]); break; diff --git a/yang/clixon/clixon-restconf@2022-08-01.yang b/yang/clixon/clixon-restconf@2022-08-01.yang index 2b85634f..6e8c112d 100644 --- a/yang/clixon/clixon-restconf@2022-08-01.yang +++ b/yang/clixon/clixon-restconf@2022-08-01.yang @@ -289,7 +289,7 @@ module clixon-restconf { "Selects between available connection types."; case persistent-connection { container persistent { - presence + presence "Indicates that a persistent connection is to be maintained."; } @@ -298,6 +298,25 @@ module clixon-restconf { container periodic { presence "Indicates periodic connects"; + leaf period { + type uint16; + units "minutes"; + default "60"; + description + "Duration of time between periodic connections."; + } + leaf idle-timeout { + type uint16; + units "seconds"; + default "120"; // two minutes + description + "Specifies the maximum number of seconds that + the underlying TCP session may remain idle. + A TCP session will be dropped if it is idle + for an interval longer than this number of + seconds. If set to zero, then the server + will never drop a session because it is idle."; + } } } }