For enabling a TLS-PSK-enabled VM migration, we rely on the VIR_MIGRATE_TLS migration flag and existence of ca-cert.pem on the source. If the migration flag is set and ca-cert.pem doesn't exist on the source, then Libvirt fallbacks to PSK-based migration instead of X.509. Subsequently, it handles the generation, persistent storage, and cleanup of pre-shared keys on both source and destination. For a migration session, Libvirt generates a random key of the specified length, and then stores the content, "qemu:<random key>", at <runtime_state_dir>/<vm_uuid>/keys.psk on the source host. Subsequently, it sends the key to destination by embedding it within the migration cookie. The destination's Libvirt extracts the key from the migration cookie, and then persistently store it exactly the same way as the source. Upon migration completion or any failure, both source and destination Libvirt deletes the directory containing the session's keys.psk. Signed-off-by: Abhisek Panda <abhisek.panda1@nutanix.com> --- src/qemu/qemu_migration.c | 53 ++++++++++++ src/qemu/qemu_migration_cookie.c | 125 +++++++++++++++++++++++++++++ src/qemu/qemu_migration_cookie.h | 5 ++ tests/qemumigrationcookiexmltest.c | 12 +-- 4 files changed, 190 insertions(+), 5 deletions(-) diff --git a/src/qemu/qemu_migration.c b/src/qemu/qemu_migration.c index 4a43ab83b0..72e13f854b 100644 --- a/src/qemu/qemu_migration.c +++ b/src/qemu/qemu_migration.c @@ -1503,6 +1503,35 @@ qemuMigrationSrcIsAllowedHostdev(const virDomainDef *def) } +static bool +qemuMigrationCACertExists(virQEMUDriver *driver) +{ + g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); + g_autofree char *cert_path = g_strdup_printf("%s/ca-cert.pem", cfg->migrateTLSx509certdir); + if (!virFileExists(cert_path)) + return false; + + return true; +} + + +static void +qemuMigrationDeletePSKDir(virQEMUDriver *driver, virDomainObj *vm) +{ + char uuidstr[VIR_UUID_STRING_BUFLEN]; + g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); + g_autofree char *dir_path = NULL; + + virUUIDFormat(vm->def->uuid, uuidstr); + dir_path = g_strdup_printf("%s/%s", cfg->stateDir, uuidstr); + + if (virFileIsDir(dir_path) && + virFileDeleteTree(dir_path) < 0) + VIR_WARN("Failed to delete the directory %s containing the pre-shared keys for migration of domain %s", + dir_path, vm->def->name); +} + + static int qemuDomainGetMigrationBlockers(virDomainObj *vm, int asyncJob, @@ -2725,6 +2754,10 @@ qemuMigrationSrcBeginXML(virDomainObj *vm, if (!(flags & VIR_MIGRATE_OFFLINE)) cookieFlags |= QEMU_MIGRATION_COOKIE_CAPS; + if ((flags & VIR_MIGRATE_TLS) && + !qemuMigrationCACertExists(driver)) + cookieFlags |= QEMU_MIGRATION_COOKIE_TLS_PSK; + if (!(mig = qemuMigrationCookieNew(vm->def, priv->origname))) return NULL; @@ -4232,6 +4265,9 @@ qemuMigrationSrcConfirmPhase(virQEMUDriver *driver, privJob->stats.mig.downtime = privMigJob->stats.mig.downtime; } + if ((flags & VIR_MIGRATE_TLS) && !qemuMigrationCACertExists(driver)) + qemuMigrationDeletePSKDir(driver, vm); + if (flags & VIR_MIGRATE_OFFLINE) return 0; @@ -5275,6 +5311,9 @@ qemuMigrationSrcRun(virQEMUDriver *driver, error: virErrorPreserveLast(&orig_err); + if ((flags & VIR_MIGRATE_TLS) && !qemuMigrationCACertExists(driver)) + qemuMigrationDeletePSKDir(driver, vm); + if (qemuDomainObjIsActive(vm)) { int reason; virDomainState state = virDomainObjGetState(vm, &reason); @@ -7029,6 +7068,9 @@ qemuMigrationDstFinishActive(virQEMUDriver *driver, QEMU_MIGRATION_COOKIE_STATS) < 0) VIR_WARN("Unable to encode migration cookie"); + if (flags & VIR_MIGRATE_TLS) + qemuMigrationDeletePSKDir(driver, vm); + qemuMigrationDstComplete(driver, vm, inPostCopy, VIR_ASYNC_JOB_MIGRATION_IN, vm->job); @@ -7039,6 +7081,9 @@ qemuMigrationDstFinishActive(virQEMUDriver *driver, * overwrites it. */ virErrorPreserveLast(&orig_err); + if (flags & VIR_MIGRATE_TLS) + qemuMigrationDeletePSKDir(driver, vm); + if (qemuDomainObjIsActive(vm)) { if (doKill) { qemuProcessStop(vm, VIR_DOMAIN_SHUTOFF_FAILED, @@ -7197,6 +7242,14 @@ qemuMigrationProcessUnattended(virQEMUDriver *driver, else qemuMigrationSrcComplete(driver, vm, job); + /* + * Attempt to clean up the directory containing the pre-shared keys + * for the domain. Since, we cannot determine if the migration has + * enabled the VIR_MIGRATE_TLS flag with pre-shared keys, we clean up + * the directory unconditionally. + */ + qemuMigrationDeletePSKDir(driver, vm); + qemuMigrationJobFinish(vm); if (!virDomainObjIsActive(vm)) diff --git a/src/qemu/qemu_migration_cookie.c b/src/qemu/qemu_migration_cookie.c index 7311a8294b..7734966983 100644 --- a/src/qemu/qemu_migration_cookie.c +++ b/src/qemu/qemu_migration_cookie.c @@ -20,9 +20,11 @@ #include <gnutls/gnutls.h> #include <gnutls/x509.h> +#include <inttypes.h> #include "locking/domain_lock.h" #include "virerror.h" +#include "virfile.h" #include "virlog.h" #include "virnetdevopenvswitch.h" #include "virstring.h" @@ -52,6 +54,7 @@ VIR_ENUM_IMPL(qemuMigrationCookieFlag, "allowReboot", "capabilities", "block-dirty-bitmaps", + "psk", ); @@ -149,6 +152,66 @@ G_DEFINE_AUTOPTR_CLEANUP_FUNC(qemuMigrationBlockDirtyBitmapsDisk, qemuMigrationBlockDirtyBitmapsDiskFree); +static int +qemuPersistTLSPSKHelper(int pskFD, + const char *pskPath, + const void *opaque) +{ + const char *key = opaque; + g_autofree char *psk_content = NULL; + + psk_content = g_strdup_printf("qemu:%s", key); + + if (safewrite(pskFD, psk_content, strlen(psk_content)) < 0) { + virReportSystemError(errno, + _("Unable to write the pre-shared key to file '%1$s'"), + pskPath); + return -1; + } + + return 0; +} + + +static int +qemuMigrationPersistPSK(qemuMigrationCookie *mig, virQEMUDriverConfig *cfg) +{ + char uuidstr[VIR_UUID_STRING_BUFLEN]; + g_autofree char *dir_path = NULL; + g_autofree char *key_path = NULL; + + virUUIDFormat(mig->uuid, uuidstr); + dir_path = g_strdup_printf("%s/%s", cfg->stateDir, uuidstr); + key_path = g_strdup_printf("%s/keys.psk", dir_path); + + if (virDirCreate(dir_path, 0700, cfg->user, cfg->group, + VIR_DIR_CREATE_ALLOW_EXIST) < 0) { + virReportSystemError(errno, + _("Could not create the directory %1$s for storing PSKs"), + dir_path); + goto error; + } + + if (mig->tlsPSK) { + if (virFileRewrite(key_path, S_IRUSR, cfg->user, + cfg->group, qemuPersistTLSPSKHelper, + mig->tlsPSK) < 0) + goto error; + } else { + virReportError(VIR_ERR_OPERATION_UNSUPPORTED, "%s", + _("The pre-shared key for TLS-PSK migration is missing in the migration cookie")); + goto error; + } + + return 0; + + error: + if (virFileExists(dir_path)) + virFileDeleteTree(dir_path); + return -1; +} + + void qemuMigrationCookieFree(qemuMigrationCookie *mig) { @@ -165,6 +228,7 @@ qemuMigrationCookieFree(qemuMigrationCookie *mig) g_free(mig->name); g_free(mig->lockState); g_free(mig->lockDriver); + g_free(mig->tlsPSK); g_clear_pointer(&mig->jobData, virDomainJobDataFree); virCPUDefFree(mig->cpu); qemuMigrationCookieCapsFree(mig->caps); @@ -575,6 +639,48 @@ qemuMigrationCookieAddCaps(qemuMigrationCookie *mig, } +static int +qemuMigrationCookieAddTLSPSK(qemuMigrationCookie *mig, virQEMUDriver *driver) +{ + gnutls_datum_t psk_key = {NULL, 0}; + g_autofree char *key = NULL; + size_t key_len; + int ret; + g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); + + ret = gnutls_key_generate(&psk_key, cfg->migrateTLSPSKLength); + if (ret < 0) { + virReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("Generation of a pre-shared key failed")); + return -1; + } + key_len = (psk_key.size*2) + 1; + key = g_new0(char, key_len); + + ret = gnutls_hex_encode(&psk_key, key, &key_len); + if (ret < 0) { + gnutls_free(psk_key.data); + virReportError(VIR_ERR_INTERNAL_ERROR, "%s", + _("Hex encoding of a PSK key failed")); + return -1; + } + + mig->tlsPSK = g_steal_pointer(&key); + mig->flags |= QEMU_MIGRATION_COOKIE_TLS_PSK; + + ret = qemuMigrationPersistPSK(mig, cfg); + if (ret < 0) { + gnutls_free(psk_key.data); + g_free(mig->tlsPSK); + mig->tlsPSK = NULL; + return -1; + } + + gnutls_free(psk_key.data); + return 0; +} + + static void qemuMigrationCookieGraphicsXMLFormat(virBuffer *buf, qemuMigrationCookieGraphics *grap) @@ -890,6 +996,9 @@ qemuMigrationCookieXMLFormat(virQEMUDriver *driver, if (mig->flags & QEMU_MIGRATION_COOKIE_BLOCK_DIRTY_BITMAPS) qemuMigrationCookieBlockDirtyBitmapsFormat(buf, mig->blockDirtyBitmaps); + if (mig->flags & QEMU_MIGRATION_COOKIE_TLS_PSK) + virBufferAsprintf(buf, "<migration-key>%s</migration-key>\n", mig->tlsPSK); + virBufferAdjustIndent(buf, -2); virBufferAddLit(buf, "</qemu-migration>\n"); return 0; @@ -1396,6 +1505,10 @@ qemuMigrationCookieXMLParse(qemuMigrationCookie *mig, qemuMigrationCookieBlockDirtyBitmapsParse(ctxt, mig) < 0) return -1; + if (flags & QEMU_MIGRATION_COOKIE_TLS_PSK) { + mig->tlsPSK = virXPathString("string(./migration-key[1])", ctxt); + } + return 0; } @@ -1471,6 +1584,10 @@ qemuMigrationCookieFormat(qemuMigrationCookie *mig, qemuMigrationCookieAddCaps(mig, dom, party) < 0) return -1; + if (flags & QEMU_MIGRATION_COOKIE_TLS_PSK && + qemuMigrationCookieAddTLSPSK(mig, driver) < 0) + return -1; + if (qemuMigrationCookieXMLFormat(driver, priv->qemuCaps, &buf, mig) < 0) return -1; @@ -1494,6 +1611,8 @@ qemuMigrationCookieParse(virQEMUDriver *driver, unsigned int flags) { g_autoptr(qemuMigrationCookie) mig = NULL; + g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver); + int ret; /* Parse & validate incoming cookie (if any) */ if (cookiein && cookieinlen && @@ -1537,6 +1656,12 @@ qemuMigrationCookieParse(virQEMUDriver *driver, } } + if ((flags & QEMU_MIGRATION_COOKIE_TLS_PSK) && mig->tlsPSK) { + ret = qemuMigrationPersistPSK(mig, cfg); + if (ret < 0) + return NULL; + } + if (vm && flags & QEMU_MIGRATION_COOKIE_STATS && mig->jobData && vm->job->current) mig->jobData->operation = vm->job->current->operation; diff --git a/src/qemu/qemu_migration_cookie.h b/src/qemu/qemu_migration_cookie.h index 254372234d..fd3b4c5a56 100644 --- a/src/qemu/qemu_migration_cookie.h +++ b/src/qemu/qemu_migration_cookie.h @@ -35,6 +35,7 @@ typedef enum { QEMU_MIGRATION_COOKIE_FLAG_ALLOW_REBOOT, QEMU_MIGRATION_COOKIE_FLAG_CAPS, QEMU_MIGRATION_COOKIE_FLAG_BLOCK_DIRTY_BITMAPS, + QEMU_MIGRATION_COOKIE_FLAG_TLS_PSK, QEMU_MIGRATION_COOKIE_FLAG_LAST } qemuMigrationCookieFlags; @@ -53,6 +54,7 @@ typedef enum { QEMU_MIGRATION_COOKIE_CPU = (1 << QEMU_MIGRATION_COOKIE_FLAG_CPU), QEMU_MIGRATION_COOKIE_CAPS = (1 << QEMU_MIGRATION_COOKIE_FLAG_CAPS), QEMU_MIGRATION_COOKIE_BLOCK_DIRTY_BITMAPS = (1 << QEMU_MIGRATION_COOKIE_FLAG_BLOCK_DIRTY_BITMAPS), + QEMU_MIGRATION_COOKIE_TLS_PSK = (1 << QEMU_MIGRATION_COOKIE_FLAG_TLS_PSK), } qemuMigrationCookieFeatures; typedef struct _qemuMigrationCookieGraphics qemuMigrationCookieGraphics; @@ -171,6 +173,9 @@ struct _qemuMigrationCookie { /* If flags & QEMU_MIGRATION_COOKIE_BLOCK_DIRTY_BITMAPS */ GSList *blockDirtyBitmaps; + + /* If flags & QEMU_MIGRATION_COOKIE_TLS_PSK */ + char *tlsPSK; }; diff --git a/tests/qemumigrationcookiexmltest.c b/tests/qemumigrationcookiexmltest.c index bc0f68b8c5..ee91c5d8b1 100644 --- a/tests/qemumigrationcookiexmltest.c +++ b/tests/qemumigrationcookiexmltest.c @@ -161,7 +161,7 @@ testQemuMigrationCookieParse(const void *opaque) } /* set all flags so that formatter attempts to format everything */ - data->cookie->flags = ~0; + data->cookie->flags = ~QEMU_MIGRATION_COOKIE_TLS_PSK; if (qemuMigrationCookieXMLFormat(&driver, priv->qemuCaps, @@ -225,15 +225,17 @@ testQemuMigrationCookieDom2XML(const char *namesuffix, * - lockstate: internals are NULL in tests, causes crash * - nbd: monitor not present * - dirty bitmaps: monitor not present + * - tls-psk: monitor not present */ unsigned int cookiePopulateFlagMask = QEMU_MIGRATION_COOKIE_LOCKSTATE | QEMU_MIGRATION_COOKIE_NBD | - QEMU_MIGRATION_COOKIE_BLOCK_DIRTY_BITMAPS; + QEMU_MIGRATION_COOKIE_BLOCK_DIRTY_BITMAPS | + QEMU_MIGRATION_COOKIE_TLS_PSK; data->cookiePopulateFlags = ~cookiePopulateFlagMask; } if (cookieParseFlags == 0) - data->cookieParseFlags = ~0; + data->cookieParseFlags = ~QEMU_MIGRATION_COOKIE_TLS_PSK; data->inStatus = g_strconcat(abs_srcdir, "/", domxml, NULL); @@ -279,7 +281,7 @@ testQemuMigrationCookieXML2XML(const char *name, int ret = 0; if (cookieParseFlags == 0) - data->cookieParseFlags = ~0; + data->cookieParseFlags = ~QEMU_MIGRATION_COOKIE_TLS_PSK; data->inStatus = g_strconcat(abs_srcdir, "/", statusxml, NULL); data->infile = g_strconcat(abs_srcdir, "/qemumigrationcookiexmldata/", @@ -381,7 +383,7 @@ testQemuMigrationCookieXML2XMLBitmaps(const char *name, int ret = 0; if (cookieParseFlags == 0) - data->cookieParseFlags = ~0; + data->cookieParseFlags = ~QEMU_MIGRATION_COOKIE_TLS_PSK; data->inStatus = g_strconcat(abs_srcdir, "/", statusxml, NULL); data->infile = g_strconcat(abs_srcdir, "/qemumigrationcookiexmldata/", -- 2.43.7