Files
wlan-ucentral-client/main.c
John Crispin cb17a7819f ucentral-client: Add command-line options for certificate paths
Allow certificate and CA file paths to be specified via -c and -C
options instead of hardcoded operational certificate paths.

Signed-off-by: John Crispin <john@phrozen.org>
2025-11-25 07:58:05 +01:00

466 lines
10 KiB
C

/* SPDX-License-Identifier: BSD-3-Clause */
#include <string.h>
#include <signal.h>
#include <getopt.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <libubox/uloop.h>
#include "ucentral.h"
#define PUBLIC_IP_FILE "/tmp/public_ip"
static int reconnect_timeout;
static int bad_CN;
static struct lws_context *context;
static struct uloop_timeout periodic;
static struct uloop_timeout watchdog;
static struct uloop_fd sock;
struct lws *websocket = NULL;
time_t conn_time;
struct runqueue adminqueue;
struct runqueue runqueue;
struct runqueue applyqueue;
struct runqueue telemetryqueue;
int reconnect_time = 0;
struct per_vhost_data__minimal {
struct lws_context *context;
struct lws_vhost *vhost;
const struct lws_protocols *protocol;
lws_sorted_usec_list_t sul;
struct lws_client_connect_info i;
struct lws *client_wsi;
};
static const lws_retry_bo_t retry = {
.secs_since_valid_ping = 60,
.secs_since_valid_hangup = 70,
};
struct client_config client = {
.server = "localhost",
.port = 11783,
.path = "/",
.serial = "00:11:22:33:44:55",
.firmware = "v1.0",
.debug = 0,
.hostname_validate = 0,
.cert = UCENTRAL_CONFIG"operational.pem",
.ca = UCENTRAL_CONFIG"operational.ca",
};
void
set_conn_time(void)
{
struct timespec tp;
clock_gettime(CLOCK_MONOTONIC, &tp);
conn_time = tp.tv_sec;
}
static int
get_reconnect_timeout(void)
{
#define BAD_CN_RECONNECT (60 * 30)
#define MAX_RECONNECT (60 * 15)
if (bad_CN) {
ULOG_INFO("BAD CN - next reconnect in %ds\n", BAD_CN_RECONNECT);
reconnect_time = BAD_CN_RECONNECT;
} else {
reconnect_time = reconnect_timeout++;
reconnect_time *= 10;
if (reconnect_time >= MAX_RECONNECT)
reconnect_time = MAX_RECONNECT;
ULOG_INFO("next reconnect in %ds\n", reconnect_time);
}
return reconnect_time * LWS_US_PER_SEC;
}
static int
validate_CN(char *CN)
{
int CN_len;
int server_len;
int ret;
if (!strcmp(client.server, CN))
return 0;
if (strncmp(CN, "*.", 2))
return -1;
CN_len = strlen(CN);
server_len = strlen(client.server);
if (server_len < CN_len)
return -1;
ret = strcmp(&CN[1], &client.server[server_len - CN_len + 1]);
if (!ret)
ULOG_INFO("server is using a wildcard certificate\n");
return ret;
}
static void
sul_connect_attempt(struct lws_sorted_usec_list *sul)
{
struct per_vhost_data__minimal *vhd;
ubus_set_client_status("offline");
vhd = lws_container_of(sul, struct per_vhost_data__minimal, sul);
vhd->i.context = vhd->context;
vhd->i.port = client.port;
vhd->i.address = client.server;
vhd->i.path = client.path;
vhd->i.host = vhd->i.address;
vhd->i.origin = vhd->i.address;
vhd->i.ssl_connection = LCCSCF_USE_SSL | LCCSCF_SKIP_SERVER_CERT_HOSTNAME_CHECK;
if (client.selfsigned)
vhd->i.ssl_connection |= LCCSCF_ALLOW_SELFSIGNED;
vhd->i.protocol = "ucentral-broker";
vhd->i.pwsi = &vhd->client_wsi;
ULOG_INFO("trying to connect\n");
if (!lws_client_connect_via_info(&vhd->i)) {
ULOG_INFO("connect failed\n");
lws_sul_schedule(vhd->context, 0, &vhd->sul,
sul_connect_attempt, get_reconnect_timeout());
}
}
static unsigned int
uloop_event_to_pfd(unsigned int e)
{
int ret = 0;
if (e & ULOOP_READ)
ret |= POLLIN;
if (e & ULOOP_WRITE)
ret |= POLLOUT;
return ret;
}
static void
sock_cb(struct uloop_fd *fd, unsigned int revents)
{
struct pollfd pfd;
pfd.events = uloop_event_to_pfd(fd->flags);
pfd.revents = uloop_event_to_pfd(revents);
pfd.fd = fd->fd;
if (fd->eof)
pfd.revents |= POLLHUP;
if (fd->error)
pfd.revents |= LWS_POLLHUP;
lws_service_fd(context, &pfd);
for (int count = 30; count && !lws_service_adjust_timeout(context, 1, 0); --count)
lws_plat_service_tsi(context, -1, 0);
}
static int
callback_broker(struct lws *wsi, enum lws_callback_reasons reason,
void *user, void *in, size_t len)
{
struct per_vhost_data__minimal *vhd =
(struct per_vhost_data__minimal *)
lws_protocol_vh_priv_get(lws_get_vhost(wsi),
lws_get_protocol(wsi));
int r = 0;
struct lws_pollargs *in_pollargs = (struct lws_pollargs *)in;
union lws_tls_cert_info_results ci;
struct stat s;
switch (reason) {
case LWS_CALLBACK_PROTOCOL_INIT:
vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi),
lws_get_protocol(wsi),
sizeof(struct per_vhost_data__minimal));
vhd->context = lws_get_context(wsi);
vhd->protocol = lws_get_protocol(wsi);
vhd->vhost = lws_get_vhost(wsi);
sul_connect_attempt(&vhd->sul);
break;
case LWS_CALLBACK_PROTOCOL_DESTROY:
lws_sul_cancel(&vhd->sul);
return r;
case LWS_CALLBACK_ADD_POLL_FD:
case LWS_CALLBACK_CHANGE_MODE_POLL_FD: {
int event = 0;
if (in_pollargs->events & POLLIN)
event |= ULOOP_READ;
if (in_pollargs->events & POLLOUT)
event |= ULOOP_WRITE;
sock.fd = in_pollargs->fd;
sock.cb = sock_cb;
uloop_fd_add(&sock, event);
return 0;
}
case LWS_CALLBACK_DEL_POLL_FD:
uloop_fd_delete(&sock);
return 0;
case LWS_CALLBACK_CLIENT_ESTABLISHED:
ubus_set_client_status("online");
ULOG_INFO("connection established\n");
if (!client.selfsigned && client.hostname_validate) {
if (!lws_tls_peer_cert_info(wsi, LWS_TLS_CERT_INFO_VALIDITY_TO, &ci, 0))
ULOG_INFO(" Peer Cert Valid to : %s", ctime(&ci.time));
if (!lws_tls_peer_cert_info(wsi, LWS_TLS_CERT_INFO_ISSUER_NAME,
&ci, sizeof(ci.ns.name)))
ULOG_INFO(" Peer Cert issuer : %s\n", ci.ns.name);
if (!lws_tls_peer_cert_info(wsi, LWS_TLS_CERT_INFO_COMMON_NAME, &ci, sizeof(ci.ns.name)))
ULOG_INFO(" Peer Cert CN : %s\n", ci.ns.name);
if (validate_CN(ci.ns.name)) {
ULOG_INFO("CN does not match");
bad_CN = 1;
// lws_wsi_close(wsi, LWS_TO_KILL_SYNC);
return -1;
}
}
bad_CN = 0;
reconnect_timeout = 1;
set_conn_time();
websocket = wsi;
remove(PUBLIC_IP_FILE);
connect_send();
crashlog_init();
if (!stat("/ucentral.upgrade", &s)) {
consolelog_init();
unlink("/ucentral.upgrade");
}
event_backlog();
break;
case LWS_CALLBACK_CLIENT_RECEIVE:
proto_handle((char *) in);
break;
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
ULOG_ERR("connection error: %s\n",
in ? (char *)in : "(null)");
#ifdef __clang_analyzer__
__attribute__ ((fallthrough));
#endif
case LWS_CALLBACK_CLIENT_CLOSED:
ULOG_INFO("connection closed\n");
websocket = NULL;
set_conn_time();
vhd->client_wsi = NULL;
lws_sul_schedule(vhd->context, 0, &vhd->sul,
sul_connect_attempt, get_reconnect_timeout());
break;
default:
break;
}
return lws_callback_http_dummy(wsi, reason, user, in, len);
}
static const struct
lws_protocols protocols[] = {
{ "ucentral-broker", callback_broker, 0, 32 * 1024, 0, NULL, 0},
{ }
};
static void
periodic_cb(struct uloop_timeout *t)
{
struct pollfd pfd = { .events = POLLIN | POLLOUT };
lws_service_fd(context, &pfd);
lws_service_tsi(context, -1, 0);
uloop_timeout_set(t, 100);
}
static void
watchdog_cb(struct uloop_timeout *t)
{
static int disconnected;
if (!websocket) {
disconnected++;
if (disconnected > 5) {
FILE *fp = fopen("/tmp/ucentral.restart", "w+");
fclose(fp);
ULOG_ERR("disconnected for more than 5m, restarting the client.");
exit(1);
}
} else {
disconnected = 0;
}
uloop_timeout_set(t, 60 * 1000);
}
static int print_usage(const char *daemon)
{
fprintf(stderr, "Usage: %s [options]\n"
"\t-i <insecure/selfsigned>\n"
"\t-S <serial>\n"
"\t-s <server>\n"
"\t-P <port>\n"
"\t-d <debug>\n"
"\t-f <firmware>\n"
"\t-h <hostname validation>\n"
"\t-r <boot in recovery mode>\n"
"\t-v <venue>\n"
"\t-c <cert>\n"
"\t-C <CA>\n",
daemon);
return -1;
}
int main(int argc, char **argv)
{
struct lws_context_creation_info info;
struct stat s = {};
int logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE | LLL_CLIENT;
struct stat st;
int ch;
int apply = 1;
while ((ch = getopt(argc, argv, "S:s:P:v:f:H:dirb:c:C:h")) != -1) {
switch (ch) {
case 's':
client.server = optarg;
break;
case 'f':
client.firmware = optarg;
break;
case 'P':
client.port = atoi(optarg);
break;
case 'd':
client.debug = 1;
logs |= LLL_DEBUG;
break;
case 'v':
client.path = optarg;
break;
case 'S':
client.serial = optarg;
break;
case 'b':
client.boot_cause = optarg;
break;
case 'c':
client.cert = optarg;
break;
case 'C':
client.ca = optarg;
break;
case 'i':
client.selfsigned = 1;
break;
case 'r':
client.recovery = 1;
break;
case 'n':
apply = 0;
break;
case 'h':
client.hostname_validate = 1;
break;
default:
return print_usage(*argv);
}
}
ulog_open(ULOG_STDIO | ULOG_SYSLOG, LOG_DAEMON, "ucentral");
if (!client.debug)
ulog_threshold(LOG_INFO);
if (!stat("/tmp/ucentral.restart", &s)) {
apply = 0;
ULOG_INFO("Starting recovery mode\n");
unlink("/tmp/ucentral.restart");
client.boot_cause = "recovery";
}
if (!stat("/ucentral.upgrade", &s))
client.boot_cause = "upgradefailed";
if (!stat("/tmp/crashlog", &s))
client.boot_cause = "crashlog";
runqueue_init(&adminqueue);
adminqueue.max_running_tasks = 1;
runqueue_init(&runqueue);
runqueue.max_running_tasks = 1;
runqueue_init(&applyqueue);
applyqueue.max_running_tasks = 1;
runqueue_init(&telemetryqueue);
telemetryqueue.max_running_tasks = 1;
config_init(apply, 0);
lws_set_log_level(logs, NULL);
memset(&info, 0, sizeof info);
info.port = CONTEXT_PORT_NO_LISTEN;
info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT;
info.client_ssl_cert_filepath = client.cert;
if (!stat(UCENTRAL_CONFIG"key.pem", &st))
info.client_ssl_private_key_filepath = UCENTRAL_CONFIG"key.pem";
info.ssl_ca_filepath = client.ca;
info.protocols = protocols;
info.fd_limit_per_thread = 1 + 1 + 1;
info.connect_timeout_secs = 30;
info.retry_and_idle_policy = &retry;
set_conn_time();
context = lws_create_context(&info);
if (!context) {
ULOG_INFO("failed to start LWS context\n");
return -1;
}
uloop_init();
ubus_init();
periodic.cb = periodic_cb;
uloop_timeout_set(&periodic, 100);
watchdog.cb = watchdog_cb;
uloop_timeout_set(&watchdog, 60 * 1000);
lws_service(context, 0);
uloop_run();
uloop_done();
proto_free();
runqueue_kill(&adminqueue);
runqueue_kill(&runqueue);
runqueue_kill(&applyqueue);
runqueue_kill(&telemetryqueue);
lws_context_destroy(context);
ubus_deinit();
config_deinit();
return 0;
}