537 lines
14 KiB
C
537 lines
14 KiB
C
/*
|
|
*
|
|
***** BEGIN LICENSE BLOCK *****
|
|
|
|
Copyright (C) 2022 Olof Hagsand and Rubicon Communications, LLC (Netgate)
|
|
|
|
This file is part of CLIXON.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
|
|
Alternatively, the contents of this file may be used under the terms of
|
|
the GNU General Public License Version 3 or later (the "GPL"),
|
|
in which case the provisions of the GPL are applicable instead
|
|
of those above. If you wish to allow use of your version of this file only
|
|
under the terms of the GPL, and not to allow others to
|
|
use your version of this file under the terms of Apache License version 2,
|
|
indicate your decision by deleting the provisions above and replace them with
|
|
the notice and other provisions required by the GPL. If you do not delete
|
|
the provisions above, a recipient may use your version of this file under
|
|
the terms of any one of the Apache License version 2 or the GPL.
|
|
|
|
***** END LICENSE BLOCK *****
|
|
|
|
See RFC 8071 NETCONF Call Home and RESTCONF Call Home
|
|
|
|
device/server client
|
|
+-----------------+ 1) tcp connect +-----------------+
|
|
| clixon_restconf | ----------------> | callhome-client | <------ 3) HTTP
|
|
| | 2) tls | |
|
|
+-----------------+ <--------------- +-----------------+
|
|
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "clixon_config.h" /* generated by config & autoconf */
|
|
#endif
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <unistd.h>
|
|
#include <string.h>
|
|
#include <errno.h>
|
|
#include <syslog.h>
|
|
#include <signal.h>
|
|
#include <netdb.h> /* gethostbyname */
|
|
#include <arpa/inet.h> /* inet_pton */
|
|
#include <netinet/tcp.h> /* TCP_NODELAY */
|
|
|
|
#include <openssl/ssl.h>
|
|
|
|
/* cligen */
|
|
#include <cligen/cligen.h>
|
|
|
|
/* clixon */
|
|
#include "clixon/clixon.h"
|
|
|
|
#define UTIL_TLS_OPTS "hD:f:F:a:p:c:C:k:"
|
|
|
|
#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 */
|
|
} tls_accept_handle;
|
|
|
|
/* User connection-specific data handle */
|
|
typedef struct {
|
|
int sd_s; /* data socket */
|
|
SSL *sd_ssl; /* SSL connection data */
|
|
} tls_session_data;
|
|
|
|
/*! 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;
|
|
}
|
|
|
|
/*! Client data socket
|
|
*/
|
|
static int
|
|
tls_input_cb(int s,
|
|
void *arg)
|
|
{
|
|
int retval = -1;
|
|
tls_session_data *sd = (tls_session_data *)arg;
|
|
SSL *ssl;
|
|
char buf[1024];
|
|
int n;
|
|
|
|
clicon_debug(1, "%s", __FUNCTION__);
|
|
ssl = sd->sd_ssl;
|
|
/* get reply & decrypt */
|
|
if ((n = SSL_read(ssl, buf, sizeof(buf))) < 0){
|
|
clicon_err(OE_XML, errno, "SSL_read");
|
|
goto done;
|
|
}
|
|
if (n == 0){
|
|
clicon_debug(1, "%s closed", __FUNCTION__);
|
|
goto done;
|
|
}
|
|
buf[n] = 0;
|
|
fprintf(stdout, "%s\n", buf);
|
|
SSL_shutdown(ssl);
|
|
SSL_free(ssl);
|
|
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 */
|
|
done:
|
|
return retval;
|
|
}
|
|
|
|
/*! Create ssl connection, select alpn, connect and verify
|
|
*/
|
|
static int
|
|
tls_ssl_init_connect(SSL_CTX *ctx,
|
|
int s,
|
|
SSL **sslp)
|
|
{
|
|
int retval = -1;
|
|
SSL *ssl = NULL;
|
|
unsigned char protos[10];
|
|
int ret;
|
|
int verify;
|
|
int sslerr;
|
|
|
|
/* create new SSL connection state */
|
|
if ((ssl = SSL_new(ctx)) == NULL){
|
|
clicon_err(OE_SSL, 0, "SSL_new.");
|
|
goto done;
|
|
}
|
|
SSL_set_fd(ssl, s); /* attach the socket descriptor */
|
|
SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
|
|
|
|
protos[0] = 8;
|
|
strncpy((char*)&protos[1], "http/1.1", 9);
|
|
if ((retval = SSL_set_alpn_protos(ssl, protos, 9)) != 0){
|
|
clicon_err(OE_SSL, retval, "SSL_set_alpn_protos.");
|
|
goto done;
|
|
}
|
|
#if 0
|
|
SSL_get0_next_proto_negotiated(conn_.tls.ssl, &next_proto, &next_proto_len);
|
|
SSL_get0_alpn_selected(conn_.tls.ssl, &next_proto, &next_proto_len);
|
|
#endif
|
|
|
|
/* perform the connection
|
|
TLSEXT_TYPE_application_layer_protocol_negotiation
|
|
int SSL_set_alpn_protos(SSL *ssl, const unsigned char *protos,
|
|
unsigned int protos_len);
|
|
see
|
|
https://www.openssl.org/docs/man3.0/man3/SSL_CTX_set_alpn_select_cb.html
|
|
*/
|
|
if ((ret = SSL_connect(ssl)) < 1){
|
|
sslerr = SSL_get_error(ssl, ret);
|
|
clicon_debug(1, "%s SSL_read() n:%d errno:%d sslerr:%d", __FUNCTION__, ret, errno, sslerr);
|
|
|
|
switch (sslerr){
|
|
case SSL_ERROR_SSL: /* 1 */
|
|
goto done;
|
|
break;
|
|
default:
|
|
clicon_err(OE_XML, errno, "SSL_connect");
|
|
goto done;
|
|
break;
|
|
}
|
|
}
|
|
/* check certificate verification result */
|
|
verify = SSL_get_verify_result(ssl);
|
|
switch (verify) {
|
|
case X509_V_OK:
|
|
break;
|
|
default:
|
|
clicon_err(OE_SSL, errno, "verify problems: %d", verify);
|
|
goto done;
|
|
}
|
|
*sslp = ssl;
|
|
retval = 0;
|
|
done:
|
|
return retval;
|
|
}
|
|
|
|
/*! Read data from file/stdin and write to TLS data socket
|
|
*/
|
|
static int
|
|
tls_write_file(FILE *fp,
|
|
SSL *ssl)
|
|
{
|
|
int retval = -1;
|
|
char *buf = NULL;
|
|
int buflen = 1024; /* start size */
|
|
char ch;
|
|
int ret;
|
|
int sslerr;
|
|
size_t len = 0;
|
|
|
|
if ((buf = malloc(buflen)) == NULL){
|
|
clicon_err(OE_UNIX, errno, "malloc");
|
|
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);
|
|
}
|
|
retval = 0;
|
|
done:
|
|
if (buf)
|
|
free(buf);
|
|
return retval;
|
|
}
|
|
|
|
/*! Callhome-server accept socket
|
|
*/
|
|
static int
|
|
tls_accept_cb(int ss,
|
|
void *arg)
|
|
{
|
|
int retval = -1;
|
|
tls_accept_handle *ta = (tls_accept_handle *)arg;
|
|
tls_session_data *sd = NULL;
|
|
int s;
|
|
struct sockaddr from = {0,};
|
|
socklen_t len;
|
|
SSL *ssl = NULL;
|
|
|
|
clicon_debug(1, "%s", __FUNCTION__);
|
|
len = sizeof(from);
|
|
if ((s = accept(ss, &from, &len)) < 0){
|
|
perror("accept");
|
|
goto done;
|
|
}
|
|
clicon_debug(1, "accepted");
|
|
if (tls_ssl_init_connect(ta->ta_ctx, s, &ssl) < 0)
|
|
goto done;
|
|
clicon_debug(1, "connected");
|
|
if ((sd = malloc(sizeof(*sd))) == NULL){
|
|
clicon_err(OE_UNIX, errno, "malloc");
|
|
goto done;
|
|
}
|
|
memset(sd, 0, sizeof(*sd));
|
|
sd->sd_s = s;
|
|
sd->sd_ssl = ssl;
|
|
/* Write HTTP request on socket */
|
|
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)
|
|
goto done;
|
|
retval = 0;
|
|
done:
|
|
return retval;
|
|
}
|
|
|
|
/*!
|
|
* out must be set to point to the selected protocol (which may be within in).
|
|
*/
|
|
static int
|
|
tls_proto_select_cb(SSL *s,
|
|
unsigned char **out,
|
|
unsigned char *outlen,
|
|
const unsigned char *in,
|
|
unsigned int inlen,
|
|
void *arg)
|
|
{
|
|
clicon_debug(1, "%s", __FUNCTION__);
|
|
return 0;
|
|
}
|
|
|
|
/*! Verify tls auth
|
|
* @see tlsauth_verify_callback
|
|
* This code needs a "X509 store", see X509_STORE_new()
|
|
* crl_file / crl_dir
|
|
*/
|
|
static int
|
|
tls_auth_verify_callback(int preverify_ok,
|
|
X509_STORE_CTX *x509_ctx)
|
|
{
|
|
return 1; /* success */
|
|
}
|
|
|
|
static SSL_CTX *
|
|
tls_ctx_init(const char *cert_path,
|
|
const char *key_path,
|
|
const char *ca_cert_path)
|
|
{
|
|
SSL_CTX *ctx = NULL;
|
|
|
|
if ((ctx = SSL_CTX_new(TLS_client_method())) == NULL) {
|
|
clicon_err(OE_SSL, 0, "SSL_CTX_new");
|
|
goto done;
|
|
}
|
|
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, tls_auth_verify_callback);
|
|
/* get peer certificate
|
|
nc_client_tls_update_opts */
|
|
if (SSL_CTX_use_certificate_file(ctx, cert_path, SSL_FILETYPE_PEM) != 1) {
|
|
clicon_err(OE_SSL, 0, "SSL_CTX_use_certificate_file");
|
|
goto done;
|
|
}
|
|
if (SSL_CTX_use_PrivateKey_file(ctx, key_path, SSL_FILETYPE_PEM) != 1) {
|
|
clicon_err(OE_SSL, 0, "SSL_CTX_use_PrivateKey_file");
|
|
goto done;
|
|
}
|
|
if (SSL_CTX_load_verify_locations(ctx, ca_cert_path, NULL) != 1) {
|
|
clicon_err(OE_SSL, 0, "SSL_CTX_load_verify_locations");
|
|
goto done;
|
|
}
|
|
(void)SSL_CTX_set_next_proto_select_cb(ctx, tls_proto_select_cb, NULL);
|
|
return ctx;
|
|
done:
|
|
return NULL;
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
usage(char *argv0)
|
|
{
|
|
fprintf(stderr, "usage:%s [options]\n"
|
|
"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 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"
|
|
"\t-c <path> \tcert\n"
|
|
"\t-C <path> \tcacert\n"
|
|
"\t-k <path> \tkey\n"
|
|
,
|
|
argv0,
|
|
RESTCONF_CH_TLS);
|
|
exit(0);
|
|
}
|
|
|
|
int
|
|
main(int argc,
|
|
char **argv)
|
|
{
|
|
int retval = -1;
|
|
clicon_handle h;
|
|
int c;
|
|
uint16_t port = RESTCONF_CH_TLS;
|
|
SSL_CTX *ctx = NULL;
|
|
int ss = -1;
|
|
int dbg = 0;
|
|
tls_accept_handle *ta = NULL;
|
|
char *input_filename = NULL;
|
|
char *ca_cert_path = NULL;
|
|
char *cert_path = NULL;
|
|
char *key_path = NULL;
|
|
FILE *fp = stdin; /* base file, stdin, can be overridden with -f */
|
|
size_t sa_len;
|
|
char *addr = "127.0.0.1";
|
|
char *family = "inet:ipv4-address";
|
|
struct sockaddr sa = {0,};
|
|
|
|
/* In the startup, logs to stderr & debug flag set later */
|
|
clicon_log_init(__FILE__, LOG_INFO, CLICON_LOG_STDERR);
|
|
|
|
if ((h = clicon_handle_init()) == NULL)
|
|
goto done;
|
|
while ((c = getopt(argc, argv, UTIL_TLS_OPTS)) != -1)
|
|
switch (c) {
|
|
case 'h':
|
|
usage(argv[0]);
|
|
break;
|
|
case 'D':
|
|
if (sscanf(optarg, "%d", &dbg) != 1)
|
|
usage(argv[0]);
|
|
break;
|
|
case 'f':
|
|
if (optarg == NULL || *optarg == '-')
|
|
usage(argv[0]);
|
|
input_filename = optarg;
|
|
break;
|
|
case 'F':
|
|
family = optarg;
|
|
break;
|
|
case 'a':
|
|
addr = optarg;
|
|
break;
|
|
case 'p':
|
|
if (sscanf(optarg, "%hu", &port) != 1)
|
|
usage(argv[0]);
|
|
break;
|
|
case 'c':
|
|
if (optarg == NULL || *optarg == '-')
|
|
usage(argv[0]);
|
|
cert_path = optarg;
|
|
break;
|
|
case 'C':
|
|
if (optarg == NULL || *optarg == '-')
|
|
usage(argv[0]);
|
|
ca_cert_path = optarg;
|
|
break;
|
|
case 'k':
|
|
if (optarg == NULL || *optarg == '-')
|
|
usage(argv[0]);
|
|
key_path = optarg;
|
|
break;
|
|
default:
|
|
usage(argv[0]);
|
|
break;
|
|
}
|
|
if (cert_path == NULL || key_path == NULL || ca_cert_path == NULL){
|
|
fprintf(stderr, "-c <cert path> and -k <key path> -C <ca-cert> are mandatory\n");
|
|
usage(argv[0]);
|
|
}
|
|
clicon_debug_init(dbg, NULL);
|
|
|
|
if (input_filename){
|
|
if ((fp = fopen(input_filename, "r")) == NULL){
|
|
clicon_err(OE_YANG, errno, "open(%s)", input_filename);
|
|
goto done;
|
|
}
|
|
}
|
|
if ((ctx = tls_ctx_init(cert_path, key_path, ca_cert_path)) == NULL)
|
|
goto done;
|
|
if (port == 0){
|
|
fprintf(stderr, "-p <port> is invalid\n");
|
|
usage(argv[0]);
|
|
goto done;
|
|
}
|
|
if (addr == NULL){
|
|
fprintf(stderr, "-a <addr> is NULL\n");
|
|
usage(argv[0]);
|
|
goto done;
|
|
}
|
|
if (clixon_inet2sin(family, addr, port, &sa, &sa_len) < 0)
|
|
goto done;
|
|
/* Bind port */
|
|
if (callhome_bind(&sa, sa_len, 1, &ss) < 0)
|
|
goto done;
|
|
clicon_debug(1, "bind");
|
|
if ((ta = malloc(sizeof(*ta))) == NULL){
|
|
clicon_err(OE_UNIX, errno, "malloc");
|
|
goto done;
|
|
}
|
|
memset(ta, 0, sizeof(*ta));
|
|
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)
|
|
goto done;
|
|
if (clixon_event_loop(h) < 0)
|
|
goto done;
|
|
retval = 0;
|
|
done:
|
|
if (ss != -1)
|
|
clixon_event_unreg_fd(ss, tls_accept_cb);
|
|
if (ta)
|
|
free(ta);
|
|
if (fp)
|
|
fclose(fp);
|
|
if (ss != -1)
|
|
close(ss);
|
|
if (ctx)
|
|
SSL_CTX_free(ctx); /* release context */
|
|
clicon_handle_exit(h); /* frees h and options (and streams) */
|
|
clixon_err_exit();
|
|
clicon_debug(1, "clixon_restconf_callhome_client pid:%u done", getpid());
|
|
clicon_log_exit(); /* Must be after last clicon_debug */
|
|
return retval;
|
|
}
|