Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions doc/libnetconf.doc
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,8 @@
* - ::nc_server_set_capab_withdefaults()
* - ::nc_server_set_capability()
* - ::nc_server_endpt_count()
* - ::nc_server_add_endpt_unix_socket_listen()
* - ::nc_server_del_endpt_unix_socket()
* - ::nc_server_set_unix_socket_path()
* - ::nc_server_get_unix_socket_path()
*
* Server Configuration
* ===
Expand Down Expand Up @@ -383,7 +383,7 @@
* You may create this data yourself or by using ::nc_server_config_add_ssh_hostkey().
*
* It is important to decide whether the users that can connect to the SSH server should be obtained from the configuration or from the system.
* If the YANG feature *local-users-supported* is enabled (the default), then the authorized users are derived from the configuration.
* If the YANG feature *local-users-supported* is enabled (the default), then the authorized users are derived from the configuration.
* When a client connects to the server, he must be found in the configuration and he must authenticate to **all** of his configured authentication methods.
* If the feature is disabled, then the system will be used to try to authenticate the client via one of the three
* methods - publickey, keyboard-interactive or password (only one of them has to succeed).
Expand Down Expand Up @@ -493,6 +493,28 @@
* - ::nc_server_config_add_tls_ctn()
* - ::nc_server_config_del_tls_ctn()
*
* UNIX Socket
* ===========
*
* A UNIX socket endpoint can be established using one of two mechanisms:
*
* 1) **Cleartext Path**: The filesystem path is explicitly stored in the configuration.
* To use this, pass a valid path string to ::nc_server_config_add_unix_socket().
*
* 2) **Hidden Path**: The filesystem path is managed via the API and is not visible
* in the YANG configuration. To use this, pass NULL as the path argument to
* ::nc_server_config_add_unix_socket(). The actual runtime path must then be set
* using ::nc_server_set_unix_socket_path().
*
* Security Recommendation
* -----------------------
* The **Hidden Path** (Option 2) is strongly recommended.
*
* If Cleartext paths are enabled, any user with permission to modify the server
* configuration can change the UNIX socket path via YANG. This allows them to
* force the server to create or overwrite arbitrary files on the filesystem
Comment thread
Roytak marked this conversation as resolved.
Outdated
* with the privileges of the server process.
*
* FD
* ==
*
Expand Down
10 changes: 8 additions & 2 deletions examples/server.c
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,19 @@ init(const char *unix_socket_path, struct ly_ctx **context, struct nc_pollsessio
ERR_MSG_CLEANUP("Error while parsing the example configuration data.\n");
}

/* add UNIX socket to the configuration tree if the path was specified */
if (unix_socket_path) {
/* add UNIX socket endpoint to the configuration tree if the path was specified */
rc = nc_server_config_add_unix_socket(*context, "unix-socket-endpt",
unix_socket_path, NULL, NULL, NULL, &config);
NULL, NULL, NULL, NULL, &config);
if (rc) {
ERR_MSG_CLEANUP("Creating UNIX socket endpoint configuration failed.\n");
}

/* use the specified path for the UNIX socket endpoint */
rc = nc_server_set_unix_socket_path("unix-socket-endpt", unix_socket_path);
if (rc) {
ERR_MSG_CLEANUP("Setting UNIX socket path failed.\n");
}
}

/* since nc_server_config_setup_data() requires all implicit nodes to be present and the example
Expand Down
37 changes: 29 additions & 8 deletions modules/libnetconf2-netconf-server@2025-11-11.yang
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ module libnetconf2-netconf-server {
description "Second revision.";
}

// Features

feature unix-socket-path {
description
"Indicates that the server supports configuration of the UNIX socket path.";
}

// Identities

/*
Expand Down Expand Up @@ -310,17 +317,31 @@ module libnetconf2-netconf-server {
and listen for incoming NETCONF connections. Client authentication
is based on the connecting process's effective user ID.";

leaf path {
type string {
length "1..107";
}
choice socket-path-config {
mandatory true;
description
"Filesystem path where the UNIX socket will be created.
The parent directory
must exist and be writable by the NETCONF server process.
"Selects how the UNIX domain socket path is determined.";
case socket-path {
if-feature "unix-socket-path";
leaf socket-path {
type string {
length "1..107";
}
description
"Relative filesystem path where the UNIX socket will be bound.
The parent directory must be set by an internal server API setting.

Example: /var/run/netconf.sock";
Example: netconf.sock";
}
}
case hidden-path {
leaf hidden-path {
type empty;
description
"Indicates that the UNIX socket path is not configured via YANG, but is instead
determined by internal server API settings.";
}
}
}

container socket-permissions {
Expand Down
136 changes: 127 additions & 9 deletions src/server_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ nc_server_config_free(struct nc_server_config *config)
struct nc_ch_client *ch_client;
struct nc_ch_endpt *ch_endpt;
LY_ARRAY_COUNT_TYPE i, j;
char *socket_path = NULL;

if (!config) {
return;
Expand All @@ -375,18 +376,32 @@ nc_server_config_free(struct nc_server_config *config)
LY_ARRAY_FOR(config->endpts, i) {
endpt = &config->endpts[i];

if (endpt->ti == NC_TI_UNIX) {
/* get the socket path before freeing the name */
socket_path = nc_server_unix_get_socket_path(endpt);
}

free(endpt->name);

/* free binds */
LY_ARRAY_FOR(endpt->binds, j) {
free(endpt->binds[j].address);
if (endpt->binds[j].sock != -1) {
close(endpt->binds[j].sock);
if (socket_path) {
/* remove the UNIX socket file */
unlink(socket_path);
}
}
free(endpt->binds[j].address);
pthread_mutex_destroy(&endpt->bind_lock);
}
LY_ARRAY_FREE(endpt->binds);

if (endpt->ti == NC_TI_UNIX) {
free(socket_path);
socket_path = NULL;
}

/* free transport specific options */
switch (endpt->ti) {
#ifdef NC_ENABLED_SSH_TLS
Expand Down Expand Up @@ -2851,26 +2866,68 @@ config_tls(const struct lyd_node *node, enum nc_operation parent_op, struct nc_e
#endif /* NC_ENABLED_SSH_TLS */

static int
config_unix_path(const struct lyd_node *node, enum nc_operation parent_op, struct nc_endpt *endpt)
config_unix_socket_path(const struct lyd_node *node, enum nc_operation parent_op, struct nc_endpt *endpt)
{
enum nc_operation op;
struct nc_bind *bind;
struct nc_server_unix_opts *opts;

NC_NODE_GET_OP(node, parent_op, &op);

opts = endpt->opts.unix;

if (op == NC_OP_DELETE) {
/* the endpoint must have a single binding, so we can just free it,
* the socket will be closed in ::nc_server_config_free() */
assert(endpt->binds);
free(endpt->binds[0].address);
LY_ARRAY_FREE(endpt->binds);

/* also clear the cleartext path flag */
opts->path_type = NC_UNIX_SOCKET_PATH_UNKNOWN;
} else if ((op == NC_OP_CREATE) || (op == NC_OP_REPLACE)) {
/* the endpoint must not have any bindings yet, so we can just create one */
assert(!endpt->binds);
LY_ARRAY_NEW_RET(LYD_CTX(node), endpt->binds, bind, 1);
bind->address = strdup(lyd_get_value(node));
NC_CHECK_ERRMEM_RET(!bind->address, 1);
bind->sock = -1;

/* also set the cleartext path flag */
opts->path_type = NC_UNIX_SOCKET_PATH_FILE;
}

return 0;
}

static int
config_unix_hidden_path(const struct lyd_node *node, enum nc_operation parent_op, struct nc_endpt *endpt)
{
enum nc_operation op;
struct nc_bind *bind;
struct nc_server_unix_opts *opts;

NC_NODE_GET_OP(node, parent_op, &op);

opts = endpt->opts.unix;

if (op == NC_OP_DELETE) {
/* the endpoint must have a single binding, so we can just free it,
* the socket will be closed in ::nc_server_config_free() */
assert(endpt->binds);
LY_ARRAY_FREE(endpt->binds);

/* also clear the hidden path flag */
opts->path_type = NC_UNIX_SOCKET_PATH_UNKNOWN;
} else if ((op == NC_OP_CREATE) || (op == NC_OP_REPLACE)) {
/* the endpoint must not have any bindings yet, so we can just create one
* and since the path is hidden, there is no address to set */
assert(!endpt->binds);
LY_ARRAY_NEW_RET(LYD_CTX(node), endpt->binds, bind, 1);
bind->sock = -1;

/* also set the hidden path flag */
opts->path_type = NC_UNIX_SOCKET_PATH_HIDDEN;
}

return 0;
Expand Down Expand Up @@ -3117,7 +3174,7 @@ config_unix_client_auth(const struct lyd_node *node, enum nc_operation parent_op
static int
config_unix(const struct lyd_node *node, enum nc_operation parent_op, struct nc_endpt *endpt)
{
struct lyd_node *n;
struct lyd_node *n, *socket_path = NULL, *hidden_path = NULL;
enum nc_operation op;

NC_NODE_GET_OP(node, parent_op, &op);
Expand All @@ -3135,9 +3192,15 @@ config_unix(const struct lyd_node *node, enum nc_operation parent_op, struct nc_
endpt->opts.unix->gid = (gid_t)-1;
}

/* config path */
NC_CHECK_RET(nc_lyd_find_child(node, "path", 1, &n));
NC_CHECK_RET(config_unix_path(n, op, endpt));
/* config mandatory unix socket path choice => only one of them can be present */
NC_CHECK_RET(nc_lyd_find_child(node, "socket-path", 0, &socket_path));
NC_CHECK_RET(nc_lyd_find_child(node, "hidden-path", 0, &hidden_path));
if (socket_path) {
NC_CHECK_RET(config_unix_socket_path(socket_path, op, endpt));
} else {
assert(hidden_path);
Comment thread
Roytak marked this conversation as resolved.
Outdated
NC_CHECK_RET(config_unix_hidden_path(hidden_path, op, endpt));
}

/* config socket permissions */
NC_CHECK_RET(nc_lyd_find_child(node, "socket-permissions", 1, &n));
Expand Down Expand Up @@ -4962,6 +5025,57 @@ nc_server_config_libnetconf2_netconf_server(const struct lyd_node *tree, int is_
return rc;
}

/**
* @brief Check if two server endpoint bindings match.
*
* They match if they use the same transport protocol, address and port.
*
* @param[in] e1 First server endpoint.
* @param[in] b1 First server endpoint binding.
* @param[in] e2 Second server endpoint.
* @param[in] b2 Second server endpoint binding.
* @return 1 if they match, 0 otherwise.
*/
static int
nc_server_config_bindings_match(const struct nc_endpt *e1, const struct nc_bind *b1,
const struct nc_endpt *e2, const struct nc_bind *b2)
{
int rc = 1;
char *addr1 = NULL, *addr2 = NULL;

if (e1->ti != e2->ti) {
/* different transport protocols */
return 0;
}

if (e1->ti == NC_TI_UNIX) {
/* UNIX sockets may have hidden or cleartext addresses */
addr1 = nc_server_unix_get_socket_path(e1);
addr2 = nc_server_unix_get_socket_path(e2);
} else {
addr1 = b1->address;
addr2 = b2->address;
}
if (!addr1 || !addr2) {
/* unable to get the address */
rc = 0;
goto cleanup;
}

if (strcmp(addr1, addr2) || (b1->port != b2->port)) {
/* different addresses or ports */
rc = 0;
goto cleanup;
}

cleanup:
if (e1->ti == NC_TI_UNIX) {
free(addr1);
free(addr2);
}
return rc;
}

/**
* @brief Atomically starts listening on new sockets and reuses existing ones.
*
Expand Down Expand Up @@ -4989,7 +5103,7 @@ nc_server_config_reconcile_sockets_listen(struct nc_server_config *old_cfg,
found = 0;
LY_ARRAY_FOR(old_cfg->endpts, struct nc_endpt, old_endpt) {
LY_ARRAY_FOR(old_endpt->binds, struct nc_bind, old_bind) {
if (!strcmp(new_bind->address, old_bind->address) && (new_bind->port == old_bind->port)) {
if (nc_server_config_bindings_match(new_endpt, new_bind, old_endpt, old_bind)) {
/* match found, reuse the socket */
new_bind->sock = old_bind->sock;
found = 1;
Expand Down Expand Up @@ -5510,6 +5624,8 @@ nc_server_config_unix_dup(const struct nc_server_unix_opts *src, struct nc_serve
*dst = calloc(1, sizeof **dst);
NC_CHECK_ERRMEM_RET(!*dst, 1);

(*dst)->path_type = src->path_type;

(*dst)->mode = src->mode;
(*dst)->uid = src->uid;
(*dst)->gid = src->gid;
Expand Down Expand Up @@ -5739,8 +5855,10 @@ nc_server_config_dup(const struct nc_server_config *src, struct nc_server_config
/* binds */
LN2_LY_ARRAY_CREATE_GOTO_WRAP(dst_endpt->binds, LY_ARRAY_COUNT(src_endpt->binds), rc, cleanup);
LY_ARRAY_FOR(src_endpt->binds, j) {
dst_endpt->binds[j].address = strdup(src_endpt->binds[j].address);
NC_CHECK_ERRMEM_GOTO(!dst_endpt->binds[j].address, rc = 1, cleanup);
if (src_endpt->binds[j].address) {
dst_endpt->binds[j].address = strdup(src_endpt->binds[j].address);
NC_CHECK_ERRMEM_GOTO(!dst_endpt->binds[j].address, rc = 1, cleanup);
}
dst_endpt->binds[j].port = src_endpt->binds[j].port;

/* mark the socket as uninitialized, it will be reassigned in ::nc_server_config_reconcile_sockets_listen() */
Expand Down
25 changes: 22 additions & 3 deletions src/server_config_util.c
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,31 @@ nc_server_config_add_unix_socket(const struct ly_ctx *ctx, const char *endpt_nam
{
int rc = 0;
char *path_fmt = NULL;
struct lys_module *mod;
const char *path_type_str;

NC_CHECK_ARG_RET(NULL, ctx, path, config, 1);
NC_CHECK_ARG_RET(NULL, ctx, config, 1);

/* create the path to the socket's path */
if (!path) {
/* creating a hidden UNIX socket path set by other means */
path_type_str = "hidden-path";
} else {
/* creating a standard UNIX socket path */
path_type_str = "socket-path";

/* check if the 'unix-socket-path' feature is enabled in the libnetconf2-netconf-server module */
mod = ly_ctx_get_module_implemented(ctx, "libnetconf2-netconf-server");
NC_CHECK_RET(!mod, 1);
if (lys_feature_value(mod, "unix-socket-path")) {
ERR(NULL, "Unable to set UNIX socket path ('unix-socket-path' feature not enabled in "
"'libnetconf2-netconf-server' module).");
return 1;
}
}

/* create the path to UNIX socket or just the empty leaf for hidden path */
NC_CHECK_ERR_RET(asprintf(&path_fmt, "/ietf-netconf-server:netconf-server/listen/endpoints/endpoint[name='%s']/"
"libnetconf2-netconf-server:unix/path", endpt_name) == -1, ERRMEM, 1);
"libnetconf2-netconf-server:unix/%s", endpt_name, path_type_str) == -1, ERRMEM, 1);
NC_CHECK_GOTO(rc = nc_server_config_create(ctx, config, path, path_fmt), cleanup);

if (!mode && !owner && !group) {
Expand Down
Loading