diff --git a/.gitignore b/.gitignore index 8652b500..69ab2bd3 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,8 @@ util/clixon_util_xml util/clixon_util_xml_mod util/clixon_util_xpath util/clixon_util_yang +util/clixon_netconf_ssh_callhome +util/clixon_netconf_ssh_callhome_client test/config.sh test/site.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 72b2546d..a6970232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,12 @@ Expected: February 2021 ### New features +* NETCONF Call Home Call Home RFC 8071 + * Solution description using openssh and utility functions, no changes to core clixon + * Example: test/test_netconf_ssh_callhome.sh + * RESTCONF Call home not done * New clixon_client API for external access + * See [client api docs](https://clixon-docs.readthedocs.io/en/latest/client.html) ### C/CLI-API changes on existing features diff --git a/test/test_netconf_ssh_callhome.sh b/test/test_netconf_ssh_callhome.sh new file mode 100755 index 00000000..999c5bec --- /dev/null +++ b/test/test_netconf_ssh_callhome.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Netconf callhome RFC 8071 + +# Magic line must be first in script (see README.md) +s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi + +# Skip it no openssh +if ! [ -x "$(command -v ssh)" ]; then + echo "...ssh not installed" + if [ "$s" = $0 ]; then exit 0; else return 0; fi # skip +fi + +: ${clixon_netconf_ssh_callhome:="clixon_netconf_ssh_callhome"} +: ${clixon_netconf_ssh_callhome_client:="clixon_netconf_ssh_callhome_client"} + +APPNAME=example +cfg=$dir/conf_yang.xml +sshdcfg=$dir/sshd.conf +rpccmd=$dir/rpccmd.xml + +# Use yang in example + +cat < $cfg + + $cfg + 42 + /usr/local/share/clixon + $IETFRFC + clixon-example + /usr/local/lib/$APPNAME/clispec + /usr/local/lib/$APPNAME/backend + example_backend.so$ + /usr/local/lib/$APPNAME/netconf + $dir/$APPNAME.sock + /usr/local/var/$APPNAME/$APPNAME.pidfile + /usr/local/var/$APPNAME + +EOF + +cat < $rpccmd + + + + +]]>]]> + + +]]>]]> +EOF + +# Make the callback after a sleep in separate thread simulating the server +# The result is not checked, only the client-side +function callhomefn() +{ + sleep 1 + + new "Start Callhome in background" + expectpart "$(sudo ${clixon_netconf_ssh_callhome} -a 127.0.0.1 -c $cfg)" 255 "" +} + +new "test params: -f $cfg" +# Bring your own backend +if [ $BE -ne 0 ]; then + # kill old backend (if any) + new "kill old backend" + sudo clixon_backend -zf $cfg + if [ $? -ne 0 ]; then + err + fi + new "start backend -s init -f $cfg" + start_backend -s init -f $cfg + + new "waiting" + wait_backend +fi + +# Start callhome server-side in background thread +callhomefn & + +new "Start Listener client" +expectpart "$(ssh -s -v -o ProxyUseFdpass=yes -o ProxyCommand="${clixon_netconf_ssh_callhome_client} -a 127.0.0.1" . netconf < $rpccmd)" 0 "urn:ietf:params:netconf:base:1.0urn:ietf:params:netconf:capability:yang-library:1.0?revision=2019-01-04&module-set-id=42urn:ietf:params:netconf:capability:candidate:1.0urn:ietf:params:netconf:capability:validate:1.1urn:ietf:params:netconf:capability:startup:1.0urn:ietf:params:netconf:capability:xpath:1.0urn:ietf:params:netconf:capability:notification:1.02]]>]]>" "]]>]]>" + +# Wait +wait + +if [ $BE -ne 0 ]; then + new "Kill backend" + # Check if premature kill + pid=$(pgrep -u root -f clixon_backend) + if [ -z "$pid" ]; then + err "backend already dead" + fi + # kill backend + stop_backend -f $cfg +fi + +new "Endtest" +endtest +rm -rf $dir diff --git a/util/Makefile.in b/util/Makefile.in index 059d78c5..0af63b05 100644 --- a/util/Makefile.in +++ b/util/Makefile.in @@ -90,6 +90,8 @@ ifdef with_restconf APPSRC += clixon_util_stream.c # Needs curl endif APPSRC += clixon_util_socket.c +APPSRC += clixon_netconf_ssh_callhome.c +APPSRC += clixon_netconf_ssh_callhome_client.c #APPSRC += clixon_util_ssl.c #APPSRC += clixon_util_grpc.c diff --git a/util/clixon_util_netconf_ssh_callhome.c b/util/clixon_netconf_ssh_callhome.c similarity index 75% rename from util/clixon_util_netconf_ssh_callhome.c rename to util/clixon_netconf_ssh_callhome.c index 00b0b658..18d42cc1 100644 --- a/util/clixon_util_netconf_ssh_callhome.c +++ b/util/clixon_netconf_ssh_callhome.c @@ -54,10 +54,16 @@ Example sshd-config (-c option):n Port 2592 - UsePrivilegeSeparation no TCPKeepAlive yes AuthorizedKeysFile ~.ssh/authorized_keys Subsystem netconf /usr/local/bin/clixon_netconf + + +ssh -s -v -o ProxyUseFdpass=yes -o ProxyCommand="clixon_netconf_ssh_callhome_client -a 127.0.0.1" . netconf +sudo clixon_netconf_ssh_callhome -a 127.0.0.1 -c /var/tmp/./test_netconf_ssh_callhome.sh/conf_yang.xml + +ssh -s -v -o ProxyUseFdpass=yes -o ProxyCommand='/home/olof/src/clixon/util/clixon_netconf_ssh_callhome_client -a 0.0.0.0' -l olof . netconf +sudo ./clixon_netconf_ssh_callhome -a 127.0.0.1 -c ./sshdcfg */ #include @@ -71,7 +77,7 @@ Example sshd-config (-c option):n #define NETCONF_CH_SSH 4334 #define SSHDBIN_DEFAULT "/usr/sbin/sshd" -#define UTIL_OPTS "hD:f:a:p:s:c:" +#define UTIL_OPTS "hD:f:a:p:s:c:C:" static int callhome_connect(struct sockaddr *sa, @@ -99,12 +105,17 @@ callhome_connect(struct sockaddr *sa, static int exec_sshd(int s, char *sshdbin, - char *configfile) + char *sshdconfigfile, + char *clixonconfigfile, + int dbg) { int retval = -1; char **argv = NULL; int i; int nr; + char *optstr = NULL; + size_t len; + const char *formatstr = "Subsystem netconf /usr/local/bin/clixon_netconf -f %s"; if (s < 0){ errno = EINVAL; @@ -116,29 +127,52 @@ exec_sshd(int s, perror("sshdbin"); goto done; } - if (configfile == NULL){ + if (sshdconfigfile == NULL){ errno = EINVAL; - perror("configfile"); + perror("sshdconfigfile"); goto done; } - nr = 7; // XXX + if (clixonconfigfile == NULL){ + errno = EINVAL; + perror("clixonconfigfile"); + goto done; + } + /* Construct subsystem string */ + len = strlen(formatstr)+strlen(clixonconfigfile)+1; + if ((optstr = malloc(len)) == NULL){ + perror("malloc"); + goto done; + } + snprintf(optstr, len, formatstr, clixonconfigfile); + + nr = 9; /* See below */ + if (dbg) + nr++; if ((argv = calloc(nr, sizeof(char *))) == NULL){ perror("calloc"); goto done; } + i = 0; + /* Note if you change here, also change in nr = above */ argv[i++] = sshdbin; argv[i++] = "-i"; /* Specifies that sshd is being run from inetd(8) */ - argv[i++] = "-d"; - argv[i++] = "-e"; - argv[i++] = "-f"; - argv[i++] = configfile; + argv[i++] = "-D"; /* Foreground ? */ + if (dbg) + argv[i++] = "-d"; /* Debug mode */ + argv[i++] = "-e"; /* write debug logs to stderr */ + argv[i++] = "-o"; /* option */ + argv[i++] = optstr; + argv[i++] = "-f"; /* config file */ + argv[i++] = sshdconfigfile; argv[i++] = NULL; assert(i==nr); if (setreuid(0, 0) < 0){ perror("setreuid"); goto done; } + close(0); + close(1); if (dup2(s, STDIN_FILENO) < 0){ perror("dup2"); return -1; @@ -167,7 +201,8 @@ usage(char *argv0) "\t-f ipv4|ipv6 \tSocket address family(ipv4 default)\n" "\t-a \tIP address (eg 1.2.3.4) - mandatory\n" "\t-p \tPort (default 4334)\n" - "\t-c \tSSHD config file - mandatory\n" + "\t-c \tCLixon config file - (default /usr/local/etc/clixon.xml)\n" + "\t-C \tSSHD config file - (default /dev/null)\n" "\t-s \tPath to sshd binary, default %s\n" , argv0, SSHDBIN_DEFAULT); @@ -186,11 +221,12 @@ main(int argc, struct sockaddr_in6 sin6 = { 0 }; struct sockaddr_in sin = { 0 }; size_t sin_len; - int debug = 0; + int dbg = 0; uint16_t port = NETCONF_CH_SSH; int s = -1; char *sshdbin = SSHDBIN_DEFAULT; - char *configfile = NULL; + char *sshdconfigfile = "/dev/null"; + char *clixonconfigfile = "/usr/local/etc/clixon.xml"; optind = 1; opterr = 0; @@ -200,7 +236,7 @@ main(int argc, usage(argv[0]); break; case 'D': - debug++; + dbg++; break; case 'f': family = optarg; @@ -211,8 +247,11 @@ main(int argc, case 'p': port = atoi(optarg); break; + case 'C': + sshdconfigfile = optarg; + break; case 'c': - configfile = optarg; + clixonconfigfile = optarg; break; case 's': sshdbin = optarg; @@ -231,11 +270,6 @@ main(int argc, usage(argv[0]); goto done; } - if (configfile == NULL){ - fprintf(stderr, "-c is NULL\n"); - usage(argv[0]); - goto done; - } if (strcmp(family, "ipv6") == 0){ sin_len = sizeof(struct sockaddr_in6); sin6.sin6_port = htons(port); @@ -256,13 +290,13 @@ main(int argc, } if (callhome_connect(sa, sin_len, &s) < 0) goto done; - if (exec_sshd(s, sshdbin, configfile) < 0) + /* For some reason this sshd returns -1 which is unclear why */ + if (exec_sshd(s, sshdbin, sshdconfigfile, clixonconfigfile, dbg) < 0) goto done; + /* Should not reach here */ if (s >= 0) close(s); retval = 0; done: return retval; } - - diff --git a/util/clixon_netconf_ssh_callhome_client.c b/util/clixon_netconf_ssh_callhome_client.c new file mode 100644 index 00000000..da5a7a68 --- /dev/null +++ b/util/clixon_netconf_ssh_callhome_client.c @@ -0,0 +1,301 @@ +/* + * + ***** BEGIN LICENSE BLOCK ***** + + Copyright (C) 2009-2016 Olof Hagsand and Benny Holmgren + Copyright (C) 2017-2019 Olof Hagsand + Copyright (C) 2020-2021 Olof Hagsand and Rubicon Communications, LLC (Netgate) + + This file is part of CLIXON. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + Alternatively, the contents of this file may be used under the terms of + the GNU General Public License Version 3 or later (the "GPL"), + in which case the provisions of the GPL are applicable instead + of those above. If you wish to allow use of your version of this file only + under the terms of the GPL, and not to allow others to + use your version of this file under the terms of Apache License version 2, + indicate your decision by deleting the provisions above and replace them with + the notice and other provisions required by the GPL. If you do not delete + the provisions above, a recipient may use your version of this file under + the terms of any one of the Apache License version 2 or the GPL. + + ***** END LICENSE BLOCK ***** + + * Create stream listen socket, bind to address, then exec ssh client + device client + +-----------------+ tcp 4321 +-----------------+ + | util_netconf_ssh| <----------------> | xxx | + | | | +-----------------+ + | exec v | 4322 | tcp + | | ssh +-----------------+ + | sshd -e | <----------------> | ssh | + +-----------------+ +-----------------+ + | stdio | stdio + +-----------------+ + | clixon_netconf | + +-----------------+ + | + +-----------------+ + | clixon_backend | + +-----------------+ + +Example sshd-config (-c option):n + ssh -s -v -o ProxyUseFdpass=yes -o ProxyCommand='/home/olof/src/clixon/util/clixon_netconf_ssh_callhome_client -a 0.0.0.0' -l olof . netconf + sudo ./clixon_netconf_ssh_callhome -a 127.0.0.1 -c ./sshdcfg + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NETCONF_CH_SSH 4334 +#define UTIL_OPTS "hD:f:a:p:" + +/* + * fdpass() + * Pass the connected file descriptor to stdout and exit. + * This is taken from: + * https://github.com/openbsd/src/blob/master/usr.bin/nc/netcat.c + * Copyright (c) 2001 Eric Jackson + * Copyright (c) 2015 Bob Beck. All rights reserved. + */ +static int +fdpass(int nfd) +{ + struct msghdr mh; + union { + struct cmsghdr hdr; + char buf[CMSG_SPACE(sizeof(int))]; + } cmsgbuf; + struct cmsghdr *cmsg; + struct iovec iov; + char c = '\0'; + ssize_t r; + struct pollfd pfd; + + /* Avoid obvious stupidity */ + if (isatty(STDOUT_FILENO)){ + perror("Cannot pass file descriptor to tty"); + return -1; + } + memset(&mh, 0, sizeof(mh)); + memset(&cmsgbuf, 0, sizeof(cmsgbuf)); + memset(&iov, 0, sizeof(iov)); + + mh.msg_control = (char*)&cmsgbuf.buf; + mh.msg_controllen = sizeof(cmsgbuf.buf); + cmsg = CMSG_FIRSTHDR(&mh); + cmsg->cmsg_len = CMSG_LEN(sizeof(int)); + cmsg->cmsg_level = SOL_SOCKET; + cmsg->cmsg_type = SCM_RIGHTS; + *(int *)CMSG_DATA(cmsg) = nfd; + + iov.iov_base = &c; + iov.iov_len = 1; + mh.msg_iov = &iov; + mh.msg_iovlen = 1; + + memset(&pfd, 0, sizeof(pfd)); + pfd.fd = STDOUT_FILENO; + pfd.events = POLLOUT; + for (;;) { + r = sendmsg(STDOUT_FILENO, &mh, 0); + if (r == -1) { + if (errno == EAGAIN || errno == EINTR) { + if (poll(&pfd, 1, -1) == -1){ + perror("poll"); + return -1; + } + continue; + } + perror("sendmsg"); + return -1; + } else if (r != 1){ + perror("sendmsg: unexpected return value"); + return -1; + } + else + break; + } + // exit(0); + return 0; +} + +/*! Create and bind stream socket + * @param[in] sa Socketaddress + * @param[in] sa_len Length of sa. Tecynicaliyu to be independent of sockaddr sa_len + * @param[in] backlog Listen backlog, queie of pending connections + * @param[out] sock Server socket (bound for accept) + */ +int +callhome_bind(struct sockaddr *sa, + size_t sin_len, + int backlog, + int *sock) +{ + int retval = -1; + int s = -1; + int on = 1; + + if (sock == NULL){ + errno = EINVAL; + perror("sock"); + goto done; + } + /* create inet socket */ + if ((s = socket(sa->sa_family, SOCK_STREAM, 0)) < 0) { + perror("socket"); + goto done; + } + if (setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (void *)&on, sizeof(on)) == -1) { + perror("setsockopt SO_KEEPALIVE"); + goto done; + } + if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (void *)&on, sizeof(on)) == -1) { + perror("setsockopt SO_REUSEADDR"); + goto done; + } + /* only bind ipv6, otherwise it may bind to ipv4 as well which is strange but seems default */ + if (sa->sa_family == AF_INET6 && + setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) == -1) { + perror("setsockopt IPPROTO_IPV6"); + goto done; + } + if (bind(s, sa, sin_len) == -1) { + perror("bind"); + goto done; + } + if (listen(s, backlog) < 0){ + perror("listen"); + goto done; + } + if (sock) + *sock = s; + retval = 0; + done: + if (retval != 0 && s != -1) + close(s); + return retval; +} + +static int +usage(char *argv0) +{ + fprintf(stderr, "usage:%s [options]\n" + "where options are\n" + "\t-h \tHelp\n" + "\t-D \tDebug\n" + "\t-f ipv4|ipv6 \tSocket address family(ipv4 default)\n" + "\t-a \tIP address (eg 1.2.3.4) - mandatory\n" + "\t-p \tPort (default 4334)\n" + , + argv0); + exit(0); +} + +int +main(int argc, + char **argv) +{ + int retval = -1; + int c; + char *family = "ipv4"; + char *addr = NULL; + struct sockaddr *sa; + struct sockaddr_in6 sin6 = { 0 }; + struct sockaddr_in sin = { 0 }; + struct sockaddr from = {0,}; + socklen_t len; + size_t sin_len; + int dbg = 0; + uint16_t port = NETCONF_CH_SSH; + int ss = -1; /* server socket */ + int s = -1; /* accepted session socket */ + + optind = 1; + opterr = 0; + while ((c = getopt(argc, argv, UTIL_OPTS)) != -1) + switch (c) { + case 'h': + usage(argv[0]); + break; + case 'D': + dbg++; /* not used */ + break; + case 'f': + family = optarg; + break; + case 'a': + addr = optarg; + break; + case 'p': + port = atoi(optarg); + break; + default: + usage(argv[0]); + break; + } + if (port == 0){ + fprintf(stderr, "-p is invalid\n"); + usage(argv[0]); + goto done; + } + if (addr == NULL){ + fprintf(stderr, "-a is NULL\n"); + usage(argv[0]); + goto done; + } + if (strcmp(family, "ipv6") == 0){ + sin_len = sizeof(struct sockaddr_in6); + sin6.sin6_port = htons(port); + sin6.sin6_family = AF_INET6; + inet_pton(AF_INET6, addr, &sin6.sin6_addr); + sa = (struct sockaddr *)&sin6; + } + else if (strcmp(family, "ipv4") == 0){ + sin_len = sizeof(struct sockaddr_in); + sin.sin_family = AF_INET; + sin.sin_port = htons(port); + sin.sin_addr.s_addr = inet_addr(addr); + sa = (struct sockaddr *)&sin; + } + else{ + fprintf(stderr, "-f <%s> is invalid family\n", family); + goto done; + } + /* Bind port */ + if (callhome_bind(sa, sin_len, 1, &ss) < 0) + goto done; + /* Wait until connect */ + len = sizeof(from); + if ((s = accept(ss, &from, &len)) < 0){ + perror("accept"); + goto done; + } + /* s Pass the first connected socket using sendmsg(2) to stdout and exit. */ + if (fdpass(s) < 0) + goto done; + retval = 0; + done: + return retval; +} + +