diff --git a/backend/src/client.c b/backend/src/client.c index e3fb443..332135b 100644 --- a/backend/src/client.c +++ b/backend/src/client.c @@ -639,8 +639,9 @@ static void sink_accept(struct csink *s) /* Transparent source spoofing */ -// Runs ip(8) without a shell. Returns its exit status, -1 if it did not run. -static int run_ip(const char *const argv[]) +// Runs a program without a shell, output discarded. Returns its exit status, +// -1 if it did not run. +static int run_cmd(const char *const argv[]) { pid_t pid = fork(); if (pid < 0) @@ -651,7 +652,7 @@ static int run_ip(const char *const argv[]) dup2(nul, STDOUT_FILENO); dup2(nul, STDERR_FILENO); } - execvp("ip", (char *const *)argv); + execvp(argv[0], (char *const *)argv); _exit(127); } int st; @@ -676,16 +677,16 @@ static void spoof_routing(int add) // Remove copies left by an earlier run, then add exactly one. for (int k = 0; k < 16; k++) { const char *del[] = { "ip", fam, "rule", "del", "pref", tab, NULL }; - if (run_ip(del) != 0) + if (run_cmd(del) != 0) break; } const char *flush[] = { "ip", fam, "route", "flush", "table", tab, NULL }; - run_ip(flush); + run_cmd(flush); if (!add) continue; const char *rule[] = { "ip", fam, "rule", "add", "pref", tab, "from", fams[i][1], "iif", "lo", "lookup", tab, NULL }; const char *route[] = { "ip", fam, "route", "replace", "local", fams[i][2], "dev", "lo", "table", tab, NULL }; - int ok = run_ip(rule) == 0 && run_ip(route) == 0; + int ok = run_cmd(rule) == 0 && run_cmd(route) == 0; if (i == 0) ok4 = ok; else if (!ok) @@ -835,23 +836,86 @@ static int process_ctl_input(void) /* Session setup */ +// libssh2 before 1.11 signs RSA keys only with SHA-1 ("ssh-rsa"), which +// OpenSSH 8.8+ rejects by default. +static int rsa_usable(void) +{ + return libssh2_version(0x010b00) != NULL; +} + +// Identifies RSA keys by the public key file or a PEM private key header. +static int key_is_rsa(const char *path) +{ + char pub[300], head[64] = ""; + snprintf(pub, sizeof(pub), "%s.pub", path); + FILE *f = fopen(pub, "r"); + if (!f) + f = fopen(path, "r"); + if (!f) + return 0; + size_t n = fread(head, 1, sizeof(head) - 1, f); + head[n] = '\0'; + fclose(f); + return !strncmp(head, "ssh-rsa ", 8) || strstr(head, "BEGIN RSA PRIVATE KEY") != NULL; +} + +// Creates PB_CLIENT_KEY with ssh-keygen unless it exists. Returns 0 if usable. +static int ensure_own_key(void) +{ + if (access(PB_CLIENT_KEY, R_OK) == 0) + return 0; + char host[64], comment[96]; + if (gethostname(host, sizeof(host)) < 0) + strcpy(host, "client"); + host[sizeof(host) - 1] = '\0'; + snprintf(comment, sizeof(comment), "patchbay@%s", host); + const char *argv[] = { "ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", comment, "-f", PB_CLIENT_KEY, NULL }; + if (run_cmd(argv) != 0 || access(PB_CLIENT_KEY, R_OK) != 0) { + log_err("cannot create %s with ssh-keygen (is openssh-client installed?)", PB_CLIENT_KEY); + return -1; + } + log_warn("created %s; add its public key in the web UI (patchbayd --pubkey)", PB_CLIENT_KEY); + return 0; +} + int client_identity(const struct pb_config *c, char *out, size_t outsz) { - const char *cands[] = { c->identity_file, "/root/.ssh/id_ed25519", "/root/.ssh/id_rsa", - "/etc/ssh/ssh_host_ed25519_key", NULL }; - for (int i = 0; cands[i]; i++) { - if (!cands[i][0]) - continue; - if (access(cands[i], R_OK) == 0) { - snprintf(out, outsz, "%s", cands[i]); - return 0; - } - if (i == 0) { - log_err("IdentityFile %s is not readable", cands[i]); + if (c->identity_file[0]) { + if (access(c->identity_file, R_OK) != 0) { + log_err("IdentityFile %s is not readable", c->identity_file); return -1; } + if (key_is_rsa(c->identity_file) && !rsa_usable()) + log_warn("IdentityFile %s is an RSA key, which libssh2 %s can only sign with SHA-1; " + "the target will reject it. Use an ed25519 key or libssh2 1.11+", c->identity_file, + libssh2_version(0)); + snprintf(out, outsz, "%s", c->identity_file); + return 0; } - return -1; + + const char *cands[] = { "/root/.ssh/id_ed25519", "/root/.ssh/id_rsa", "/etc/ssh/ssh_host_ed25519_key", NULL }; + for (int i = 0; cands[i]; i++) { + if (access(cands[i], R_OK) != 0) + continue; + if (key_is_rsa(cands[i]) && !rsa_usable()) { + // A key the target rejects is worse than none: use our own instead. + static int told; + if (!told++) + log_warn("%s is an RSA key that libssh2 %s cannot sign acceptably; using %s instead", cands[i], + libssh2_version(0), PB_CLIENT_KEY); + if (ensure_own_key() < 0) + return -1; + snprintf(out, outsz, "%s", PB_CLIENT_KEY); + return 0; + } + snprintf(out, outsz, "%s", cands[i]); + return 0; + } + // No usable key at all: create our own (or reuse one created earlier). + if (ensure_own_key() < 0) + return -1; + snprintf(out, outsz, "%s", PB_CLIENT_KEY); + return 0; } static int verify_host_key(void) diff --git a/backend/src/config.h b/backend/src/config.h index 9454aac..243450f 100644 --- a/backend/src/config.h +++ b/backend/src/config.h @@ -4,6 +4,7 @@ #define PB_DEFAULT_CONF "/etc/patchbay/patchbay.conf" #define PB_DEFAULT_DB "/etc/patchbay/patchbay.db" #define PB_RUN_DIR "/run/patchbay" +#define PB_CLIENT_KEY "/etc/patchbay/id_ed25519" // created when root only has an unusable RSA key enum pb_role { ROLE_CLIENT, ROLE_TARGET }; diff --git a/examples/patchbay.conf.client b/examples/patchbay.conf.client index bf90d62..ba8fd6c 100644 --- a/examples/patchbay.conf.client +++ b/examples/patchbay.conf.client @@ -3,8 +3,10 @@ Role = client TargetHost = gateway.example.org TargetPort = 2222 -# Optional dedicated key. Without it, root's key (/root/.ssh/id_ed25519) and -# then the host key (/etc/ssh/ssh_host_ed25519_key) are used. +# Optional dedicated key. Without it, root's key (/root/.ssh/id_ed25519, then +# id_rsa) and then the host key (/etc/ssh/ssh_host_ed25519_key) are used. If +# none exists, or root's key is RSA and libssh2 is older than 1.11 (SHA-1 +# signatures, rejected by current OpenSSH), /etc/patchbay/id_ed25519 is created. # Print the public key to add in the web UI with: patchbayd --pubkey # IdentityFile = /etc/patchbay/id_ed25519 # IdentityPassphrase =