mirror of
https://dev.lirent.ru/Vatrog/vm-automation-signaling.git
synced 2026-06-26 04:36:37 +03:00
vmsig: management daemon, runtime endpoint lifecycle, roster, discovery, in-tree drivers, packaging
- core: runtime attach/detach of a per-endpoint adapter trio (runtime-safe add_adapter + vmsig_core_detach_endpoint, deferred reap) - roster: VMSIG_EV_ROSTER + CAP_ROSTER, retained per-endpoint and replayed to late subscribers - discovery: inotify trigger dir, vmid/endpoint slot allocator, host probe; vmsigd daemon with config + per-uid admission - input driver and vgpu perception built in-tree; vgpu perception as a separate library - memctx: own the supplied ro_fd (closed at detach) - deb packaging: install rules, systemd unit, tmpfiles, default config
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
/* admission.c — vmsigd coarse admission policy (see vmsigd_admission.h). Translates a uid to
|
||||
* a vmsig_grant, resolving entitled vmids to live endpoint bits via the discovery slot map. */
|
||||
#define _GNU_SOURCE
|
||||
#include "vmsigd_admission.h"
|
||||
#include "discovery.h" /* vmsig_discovery_slot_of_vmid */
|
||||
#include <string.h>
|
||||
|
||||
static const vmsigd_grant_rule* rule_for_uid(const vmsigd_config* cfg, uint32_t uid) {
|
||||
for (int i = 0; i < cfg->ngrants; i++)
|
||||
if (cfg->grants[i].uid == uid) return &cfg->grants[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
vmsig_grant vmsigd_policy(uint32_t uid, uint32_t pid, void* ud) {
|
||||
(void)pid;
|
||||
vmsigd_admission* a = ud;
|
||||
vmsig_grant g;
|
||||
memset(&g, 0, sizeof g);
|
||||
|
||||
const vmsigd_grant_rule* r = (a && a->cfg) ? rule_for_uid(a->cfg, uid) : NULL;
|
||||
if (!r) return g; /* no stanza => empty grant => REJECT */
|
||||
|
||||
g.principal = uid;
|
||||
g.source_mask = 0xFFFFFFFFu; /* coarse: control enforces source finer behind us */
|
||||
g.cap_mask = r->cap_mask;
|
||||
g.arb_prio = r->arb_prio;
|
||||
|
||||
if (r->all_vms) {
|
||||
g.endpoint_mask = ~0ull; /* covers all current + future endpoints */
|
||||
} else {
|
||||
for (int i = 0; i < r->nvmids; i++) {
|
||||
int ep = a->disc ? vmsig_discovery_slot_of_vmid(a->disc, r->vmids[i]) : -1;
|
||||
if (ep >= 0 && ep < 64) g.endpoint_mask |= (1ull << ep);
|
||||
}
|
||||
}
|
||||
return g;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/* config.c — vmsigd config parser (see vmsigd.h). INI-ish: `key = value` globals + repeated
|
||||
* `[grant uid=N]` stanzas. Pure libc; no core/vmie dependency (unit-testable in any build). */
|
||||
#define _GNU_SOURCE
|
||||
#include "vmsigd.h"
|
||||
#include "vmsig_control.h" /* VMSIG_CAP_* */
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
void vmsigd_config_defaults(vmsigd_config* c) {
|
||||
memset(c, 0, sizeof *c);
|
||||
snprintf(c->socket, sizeof c->socket, "%s", "/run/vmsig/vmsigd.sock");
|
||||
snprintf(c->watch, sizeof c->watch, "%s", "/dev/shm/vmsig");
|
||||
snprintf(c->pve_conf, sizeof c->pve_conf, "%s", "/etc/pve/qemu-server");
|
||||
snprintf(c->qmp_dir, sizeof c->qmp_dir, "%s", "/var/run/qemu-server");
|
||||
snprintf(c->slots, sizeof c->slots, "%s", "/dev/shm/vmsig/.slots");
|
||||
}
|
||||
|
||||
uint32_t vmsigd_caps_from_str(const char* s) {
|
||||
static const struct { const char* k; uint32_t bit; } map[] = {
|
||||
{ "observe", VMSIG_CAP_OBSERVE },
|
||||
{ "input", VMSIG_CAP_INPUT },
|
||||
{ "lifecycle", VMSIG_CAP_LIFECYCLE },
|
||||
{ "power", VMSIG_CAP_POWER },
|
||||
{ "vm", VMSIG_CAP_VM },
|
||||
{ "memctx", VMSIG_CAP_MEMCTX },
|
||||
{ "memwrite", VMSIG_CAP_MEMWRITE },
|
||||
{ "roster", VMSIG_CAP_ROSTER },
|
||||
};
|
||||
uint32_t mask = 0;
|
||||
while (s && *s) {
|
||||
while (*s == ',' || *s == ' ' || *s == '\t') s++;
|
||||
const char* w = s;
|
||||
while (*s && *s != ',' && *s != ' ' && *s != '\t') s++;
|
||||
size_t len = (size_t)(s - w);
|
||||
for (size_t i = 0; i < sizeof map / sizeof map[0]; i++)
|
||||
if (len == strlen(map[i].k) && strncmp(w, map[i].k, len) == 0) { mask |= map[i].bit; break; }
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
/* Trim leading/trailing whitespace in place; returns the trimmed start. */
|
||||
static char* trim(char* s) {
|
||||
while (*s == ' ' || *s == '\t' || *s == '\r') s++;
|
||||
char* e = s + strlen(s);
|
||||
while (e > s && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\r' || e[-1] == '\n')) *--e = 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
static void set_path(char* dst, size_t cap, const char* v) { snprintf(dst, cap, "%s", v); }
|
||||
|
||||
static void parse_vmids(vmsigd_grant_rule* g, const char* v) {
|
||||
g->all_vms = 0; g->nvmids = 0;
|
||||
if (strchr(v, '*')) { g->all_vms = 1; return; }
|
||||
while (*v) {
|
||||
while (*v == ',' || *v == ' ' || *v == '\t') v++;
|
||||
if (*v < '0' || *v > '9') { if (*v) v++; continue; }
|
||||
uint32_t id = (uint32_t)strtoul(v, NULL, 10);
|
||||
while (*v >= '0' && *v <= '9') v++;
|
||||
if (id && g->nvmids < VMSIGD_MAX_VMIDS) g->vmids[g->nvmids++] = id;
|
||||
}
|
||||
}
|
||||
|
||||
int vmsigd_config_parse_buf(vmsigd_config* c, const char* buf) {
|
||||
if (!c || !buf) return -1;
|
||||
char* copy = strdup(buf);
|
||||
if (!copy) return -1;
|
||||
|
||||
vmsigd_grant_rule* cur = NULL; /* current [grant] stanza, or NULL for globals */
|
||||
char* save = NULL;
|
||||
for (char* line = strtok_r(copy, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) {
|
||||
char* p = trim(line);
|
||||
if (!*p || *p == '#' || *p == ';') continue;
|
||||
|
||||
if (*p == '[') {
|
||||
cur = NULL;
|
||||
/* [grant uid=N] */
|
||||
char* u = strstr(p, "uid=");
|
||||
if (u && c->ngrants < VMSIGD_MAX_GRANTS) {
|
||||
cur = &c->grants[c->ngrants++];
|
||||
memset(cur, 0, sizeof *cur);
|
||||
cur->uid = (uint32_t)strtoul(u + 4, NULL, 10);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
char* eq = strchr(p, '=');
|
||||
if (!eq) continue;
|
||||
*eq = 0;
|
||||
char* key = trim(p);
|
||||
char* val = trim(eq + 1);
|
||||
|
||||
if (cur) {
|
||||
if (!strcmp(key, "vmids")) parse_vmids(cur, val);
|
||||
else if (!strcmp(key, "caps")) cur->cap_mask = vmsigd_caps_from_str(val);
|
||||
else if (!strcmp(key, "arb_prio")) cur->arb_prio = (uint32_t)strtoul(val, NULL, 10);
|
||||
} else {
|
||||
if (!strcmp(key, "socket")) set_path(c->socket, sizeof c->socket, val);
|
||||
else if (!strcmp(key, "watch")) set_path(c->watch, sizeof c->watch, val);
|
||||
else if (!strcmp(key, "pve_conf")) set_path(c->pve_conf, sizeof c->pve_conf, val);
|
||||
else if (!strcmp(key, "qmp_dir")) set_path(c->qmp_dir, sizeof c->qmp_dir, val);
|
||||
else if (!strcmp(key, "slots")) set_path(c->slots, sizeof c->slots, val);
|
||||
}
|
||||
}
|
||||
free(copy);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int vmsigd_config_parse_file(vmsigd_config* c, const char* path) {
|
||||
int fd = open(path, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) return -1;
|
||||
char buf[16 * 1024];
|
||||
size_t got = 0;
|
||||
for (;;) {
|
||||
ssize_t n = read(fd, buf + got, sizeof buf - 1 - got);
|
||||
if (n < 0) { close(fd); return -1; }
|
||||
if (n == 0) break;
|
||||
got += (size_t)n;
|
||||
if (got >= sizeof buf - 1) break;
|
||||
}
|
||||
close(fd);
|
||||
buf[got] = 0;
|
||||
return vmsigd_config_parse_buf(c, buf);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef VMSIGD_H
|
||||
#define VMSIGD_H
|
||||
#include <stdint.h>
|
||||
|
||||
/* vmsigd.h — private config model of the vmsig daemon.
|
||||
*
|
||||
* The daemon owns the /dev/shm/vmsig discovery namespace and serves a unix-socket control
|
||||
* plane over the signaling layer for the VMs discovered there. Its only policy is a COARSE
|
||||
* admission grant per uid (SISC: signaling is not a fine-grained access broker — the control
|
||||
* enforces per-user caps behind the grant). Entitlements are expressed in vmid terms and
|
||||
* translated to an endpoint_mask at connect time against the live slot map. */
|
||||
|
||||
#define VMSIGD_MAX_GRANTS 64
|
||||
#define VMSIGD_MAX_VMIDS 64
|
||||
#define VMSIGD_PATH_MAX 256
|
||||
|
||||
typedef struct {
|
||||
uint32_t uid;
|
||||
int all_vms; /* `vmids = *` */
|
||||
uint32_t vmids[VMSIGD_MAX_VMIDS];
|
||||
int nvmids;
|
||||
uint32_t cap_mask; /* VMSIG_CAP_* (from `caps =` keywords) */
|
||||
uint32_t arb_prio;
|
||||
} vmsigd_grant_rule;
|
||||
|
||||
typedef struct {
|
||||
char socket[VMSIGD_PATH_MAX]; /* control listener ('@' => abstract) */
|
||||
char watch[VMSIGD_PATH_MAX]; /* discovery dir (/dev/shm/vmsig) */
|
||||
char pve_conf[VMSIGD_PATH_MAX]; /* /etc/pve/qemu-server */
|
||||
char qmp_dir[VMSIGD_PATH_MAX]; /* /var/run/qemu-server */
|
||||
char slots[VMSIGD_PATH_MAX]; /* slot persistence ("" => off) */
|
||||
vmsigd_grant_rule grants[VMSIGD_MAX_GRANTS];
|
||||
int ngrants;
|
||||
} vmsigd_config;
|
||||
|
||||
/* Populate with built-in defaults. */
|
||||
void vmsigd_config_defaults(vmsigd_config* c);
|
||||
|
||||
/* Parse the INI-ish config (globals + repeated [grant uid=N] stanzas) over the defaults
|
||||
* already in `c`. Unknown keys are ignored. Returns 0, or -1 on open/usage error. */
|
||||
int vmsigd_config_parse_file(vmsigd_config* c, const char* path);
|
||||
int vmsigd_config_parse_buf (vmsigd_config* c, const char* buf); /* same, from memory (tests) */
|
||||
|
||||
/* Translate a comma/space-separated cap keyword list to a VMSIG_CAP_* mask. */
|
||||
uint32_t vmsigd_caps_from_str(const char* s);
|
||||
|
||||
#endif /* VMSIGD_H */
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef VMSIGD_ADMISSION_H
|
||||
#define VMSIGD_ADMISSION_H
|
||||
#include "vmsigd.h"
|
||||
#include "vmsig_control.h" /* vmsig_grant */
|
||||
|
||||
struct vmsig_discovery;
|
||||
|
||||
/* Admission context handed to the socket listener as policy `ud`. The config is read-only at
|
||||
* connect time; the live discovery resolves entitled vmids to their current endpoint bits. */
|
||||
typedef struct {
|
||||
const vmsigd_config* cfg;
|
||||
struct vmsig_discovery* disc;
|
||||
} vmsigd_admission;
|
||||
|
||||
/* vmsig_socket_policy: uid from SO_PEERCRED -> a coarse grant. No matching [grant uid=N]
|
||||
* stanza => empty grant (the listener rejects). `vmids = *` => endpoint_mask covers all 64;
|
||||
* a vmid list resolves each currently-attached vmid to its endpoint bit (an unbound entitled
|
||||
* vmid contributes no bit yet — the peer learns liveness via the roster). */
|
||||
vmsig_grant vmsigd_policy(uint32_t uid, uint32_t pid, void* ud);
|
||||
|
||||
#endif /* VMSIGD_ADMISSION_H */
|
||||
@@ -0,0 +1,156 @@
|
||||
/* vmsigd.c — the vmsig management daemon.
|
||||
*
|
||||
* Owns the /dev/shm/vmsig discovery namespace and serves a unix-socket control plane over the
|
||||
* signaling layer for the VMs found there. It wires nothing VM-specific: discovery hot-plugs
|
||||
* each VM's adapter trio and publishes the roster; the daemon only supplies the loop, the
|
||||
* discovery roots, the control socket, and a coarse per-uid admission policy.
|
||||
*
|
||||
* Real input/memctx actuation needs an armed library build (memctx -> vmie). A stub build
|
||||
* still runs (socket/admission/discovery machinery), but memctx will not bootstrap.
|
||||
*
|
||||
* Usage: vmsigd [--config PATH] [--socket S] [--watch DIR] [--pve-conf DIR] [--qmp-dir DIR]
|
||||
* [--slots PATH] [--foreground]
|
||||
* precedence: argv > environment (VMSIGD_*) > config file > built-in defaults. */
|
||||
#define _GNU_SOURCE
|
||||
#include "vmsig.h"
|
||||
#include "vmsig_socket.h"
|
||||
#include "discovery.h"
|
||||
#include "core_internal.h" /* core_add_source (in-repo daemon, intimate with the core) */
|
||||
#include "vmsigd.h"
|
||||
#include "vmsigd_admission.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <sys/signalfd.h>
|
||||
|
||||
static vmsig_core* g_core;
|
||||
static vmsigd_config g_cfg;
|
||||
static char g_cfg_path[VMSIGD_PATH_MAX];
|
||||
|
||||
/* Audit trace: admissions/denials, lease and memctx grants — on the loop thread, to stderr
|
||||
* (systemd routes stderr to the journal). */
|
||||
static void on_audit(void* ud, const vmsig_audit* a) {
|
||||
(void)ud;
|
||||
static const char* k[] = {
|
||||
"ADMIT", "REJECT", "DOWN_DENIED", "LEASE_GRANTED", "LEASE_DENIED",
|
||||
"LEASE_REVOKED", "LEASE_RECLAIMED", "MEMCTX_GRANTED"
|
||||
};
|
||||
const char* name = (a->kind <= VMSIG_AUDIT_MEMCTX_GRANTED) ? k[a->kind] : "?";
|
||||
fprintf(stderr, "vmsigd: audit %-14s principal=%u ep=%u cmd=%u detail=%u\n",
|
||||
name, a->principal, a->endpoint, a->cmd, a->detail);
|
||||
}
|
||||
|
||||
/* Signals arrive as fd readiness (signalfd) on the loop thread — no async-handler hazards.
|
||||
* TERM/INT => graceful stop; HUP => reload ONLY the admission table from the config file
|
||||
* (paths/socket/adapters are untouched; already-connected grants are not retroactively
|
||||
* changed — a peer reconnects to pick up a changed entitlement). */
|
||||
static void on_signal(void* user, uint32_t events) {
|
||||
(void)events;
|
||||
int sfd = *(int*)user;
|
||||
struct signalfd_siginfo si;
|
||||
while (read(sfd, &si, sizeof si) == (ssize_t)sizeof si) {
|
||||
if (si.ssi_signo == SIGINT || si.ssi_signo == SIGTERM) {
|
||||
vmsig_core_stop(g_core);
|
||||
} else if (si.ssi_signo == SIGHUP) {
|
||||
vmsigd_config fresh;
|
||||
vmsigd_config_defaults(&fresh);
|
||||
if (g_cfg_path[0] && vmsigd_config_parse_file(&fresh, g_cfg_path) == 0) {
|
||||
memcpy(g_cfg.grants, fresh.grants, sizeof g_cfg.grants);
|
||||
g_cfg.ngrants = fresh.ngrants; /* swap admission table only */
|
||||
fprintf(stderr, "vmsigd: reloaded %d grant rule(s)\n", g_cfg.ngrants);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const char* arg_val(int argc, char** argv, int* i) {
|
||||
char* a = argv[*i];
|
||||
char* eq = strchr(a, '=');
|
||||
if (eq) return eq + 1;
|
||||
if (*i + 1 < argc) { (*i)++; return argv[*i]; }
|
||||
return "";
|
||||
}
|
||||
|
||||
static void apply_env(vmsigd_config* c) {
|
||||
const char* v;
|
||||
if ((v = getenv("VMSIGD_SOCKET"))) snprintf(c->socket, sizeof c->socket, "%s", v);
|
||||
if ((v = getenv("VMSIGD_WATCH"))) snprintf(c->watch, sizeof c->watch, "%s", v);
|
||||
if ((v = getenv("VMSIGD_PVE_CONF"))) snprintf(c->pve_conf, sizeof c->pve_conf, "%s", v);
|
||||
if ((v = getenv("VMSIGD_QMP_DIR"))) snprintf(c->qmp_dir, sizeof c->qmp_dir, "%s", v);
|
||||
if ((v = getenv("VMSIGD_SLOTS"))) snprintf(c->slots, sizeof c->slots, "%s", v);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
/* config path: argv --config > env > default. */
|
||||
const char* cfg_path = getenv("VMSIGD_CONFIG");
|
||||
if (!cfg_path) cfg_path = "/etc/vmsig/vmsigd.conf";
|
||||
for (int i = 1; i < argc; i++)
|
||||
if (!strncmp(argv[i], "--config", 8)) { cfg_path = arg_val(argc, argv, &i); }
|
||||
|
||||
vmsigd_config_defaults(&g_cfg);
|
||||
vmsigd_config_parse_file(&g_cfg, cfg_path); /* missing file => defaults (not fatal) */
|
||||
snprintf(g_cfg_path, sizeof g_cfg_path, "%s", cfg_path);
|
||||
apply_env(&g_cfg);
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
char* a = argv[i];
|
||||
if (!strncmp(a, "--config", 8)) { (void)arg_val(argc, argv, &i); }
|
||||
else if (!strncmp(a, "--socket", 8)) snprintf(g_cfg.socket, sizeof g_cfg.socket, "%s", arg_val(argc, argv, &i));
|
||||
else if (!strncmp(a, "--watch", 7)) snprintf(g_cfg.watch, sizeof g_cfg.watch, "%s", arg_val(argc, argv, &i));
|
||||
else if (!strncmp(a, "--pve-conf", 10)) snprintf(g_cfg.pve_conf, sizeof g_cfg.pve_conf, "%s", arg_val(argc, argv, &i));
|
||||
else if (!strncmp(a, "--qmp-dir", 9)) snprintf(g_cfg.qmp_dir, sizeof g_cfg.qmp_dir, "%s", arg_val(argc, argv, &i));
|
||||
else if (!strncmp(a, "--slots", 7)) snprintf(g_cfg.slots, sizeof g_cfg.slots, "%s", arg_val(argc, argv, &i));
|
||||
else if (!strcmp(a, "--foreground")) { /* default; systemd Type=simple */ }
|
||||
else if (!strcmp(a, "-h") || !strcmp(a, "--help")) {
|
||||
fprintf(stderr, "usage: %s [--config P][--socket S][--watch D][--pve-conf D]"
|
||||
"[--qmp-dir D][--slots P][--foreground]\n", argv[0]);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Signals via signalfd, serviced on the loop thread. SIGPIPE ignored (dead-peer writes). */
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
sigset_t mask;
|
||||
sigemptyset(&mask);
|
||||
sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); sigaddset(&mask, SIGHUP);
|
||||
sigprocmask(SIG_BLOCK, &mask, NULL);
|
||||
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
|
||||
if (sfd < 0) { perror("vmsigd: signalfd"); return 1; }
|
||||
|
||||
vmsig_ctx* ctx = vmsig_ctx_new();
|
||||
if (!ctx) { fprintf(stderr, "vmsigd: ctx_new failed\n"); close(sfd); return 1; }
|
||||
g_core = vmsig_core_new(ctx);
|
||||
if (!g_core) { fprintf(stderr, "vmsigd: core_new failed\n"); vmsig_ctx_free(ctx); close(sfd); return 1; }
|
||||
vmsig_core_set_audit(g_core, on_audit, NULL);
|
||||
|
||||
if (core_add_source(g_core, sfd, on_signal, &sfd, NULL) != 0) {
|
||||
fprintf(stderr, "vmsigd: signal source registration failed\n");
|
||||
vmsig_core_free(g_core); vmsig_ctx_free(ctx); close(sfd); return 1;
|
||||
}
|
||||
|
||||
vmsig_discovery* disc = vmsig_discovery_new(
|
||||
g_core, g_cfg.watch, g_cfg.pve_conf, g_cfg.qmp_dir,
|
||||
g_cfg.slots[0] ? g_cfg.slots : NULL, NULL, NULL);
|
||||
if (!disc) {
|
||||
fprintf(stderr, "vmsigd: discovery_new(%s) failed\n", g_cfg.watch);
|
||||
vmsig_core_free(g_core); vmsig_ctx_free(ctx); close(sfd); return 1;
|
||||
}
|
||||
|
||||
vmsigd_admission adm = { &g_cfg, disc };
|
||||
if (vmsig_socket_attach(g_core, g_cfg.socket, vmsigd_policy, &adm) != 0) {
|
||||
fprintf(stderr, "vmsigd: socket_attach(%s) failed\n", g_cfg.socket);
|
||||
vmsig_core_free(g_core); vmsig_ctx_free(ctx); close(sfd); return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "vmsigd: serving %s (watch=%s pve=%s qmp=%s) %d grant rule(s)\n",
|
||||
g_cfg.socket, g_cfg.watch, g_cfg.pve_conf, g_cfg.qmp_dir, g_cfg.ngrants);
|
||||
int rc = vmsig_core_run(g_core);
|
||||
fprintf(stderr, "vmsigd: loop exit rc=%d\n", rc);
|
||||
|
||||
vmsig_core_free(g_core); /* reaps discovery (source on_free) + closes the socket listener */
|
||||
vmsig_ctx_free(ctx);
|
||||
close(sfd);
|
||||
return rc;
|
||||
}
|
||||
Reference in New Issue
Block a user