diff --git a/migration/meson.build b/migration/meson.build
index 4e8a9ccf3e..04e2e16239 100644
--- a/migration/meson.build
+++ b/migration/meson.build
@@ -42,3 +42,5 @@ system_ss.add(when: zstd, if_true: files('multifd-zstd.c'))
 specific_ss.add(when: 'CONFIG_SYSTEM_ONLY',
                 if_true: files('ram.c',
                                'target.c'))
+
+system_ss.add(when: rdma, if_true: files('rdma.c'))
diff --git a/migration/migration.c b/migration/migration.c
index 6b9ad4ff5f..77c301d351 100644
--- a/migration/migration.c
+++ b/migration/migration.c
@@ -25,6 +25,7 @@
 #include "sysemu/runstate.h"
 #include "sysemu/sysemu.h"
 #include "sysemu/cpu-throttle.h"
+#include "rdma.h"
 #include "ram.h"
 #include "migration/global_state.h"
 #include "migration/misc.h"
@@ -145,7 +146,7 @@ static bool transport_supports_multi_channels(MigrationAddress *addr)
     } else if (addr->transport == MIGRATION_ADDRESS_TYPE_FILE) {
         return migrate_mapped_ram();
     } else {
-        return false;
+        return addr->transport == MIGRATION_ADDRESS_TYPE_RDMA;
     }
 }
 
@@ -644,6 +645,10 @@ static void qemu_start_incoming_migration(const char *uri, bool has_channels,
         } else if (saddr->type == SOCKET_ADDRESS_TYPE_FD) {
             fd_start_incoming_migration(saddr->u.fd.str, errp);
         }
+#ifdef CONFIG_RDMA
+    } else if (addr->transport == MIGRATION_ADDRESS_TYPE_RDMA) {
+        rdma_start_incoming_migration(&addr->u.rdma, errp);
+#endif
     } else if (addr->transport == MIGRATION_ADDRESS_TYPE_EXEC) {
         exec_start_incoming_migration(addr->u.exec.args, errp);
     } else if (addr->transport == MIGRATION_ADDRESS_TYPE_FILE) {
@@ -2046,6 +2051,10 @@ void qmp_migrate(const char *uri, bool has_channels,
         } else if (saddr->type == SOCKET_ADDRESS_TYPE_FD) {
             fd_start_outgoing_migration(s, saddr->u.fd.str, &local_err);
         }
+#ifdef CONFIG_RDMA
+    } else if (addr->transport == MIGRATION_ADDRESS_TYPE_RDMA) {
+        rdma_start_outgoing_migration(s, &addr->u.rdma, &local_err);
+#endif
     } else if (addr->transport == MIGRATION_ADDRESS_TYPE_EXEC) {
         exec_start_outgoing_migration(s, addr->u.exec.args, &local_err);
     } else if (addr->transport == MIGRATION_ADDRESS_TYPE_FILE) {
diff --git a/migration/rdma.c b/migration/rdma.c
new file mode 100644
index 0000000000..09a4de7f59
--- /dev/null
+++ b/migration/rdma.c
@@ -0,0 +1,88 @@
+/*
+ * QEMU live migration via RDMA
+ *
+ * Copyright (c) 2024 HUAWEI TECHNOLOGIES CO., LTD.
+ *
+ * Authors:
+ *  Jialin Wang <wangjialin23@huawei.com>
+ *  Gonglei <arei.gonglei@huawei.com>
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2 or later.
+ * See the COPYING file in the top-level directory.
+ */
+
+#include "qemu/osdep.h"
+#include "io/channel-rdma.h"
+#include "io/channel.h"
+#include "qapi/clone-visitor.h"
+#include "qapi/qapi-types-sockets.h"
+#include "qapi/qapi-visit-sockets.h"
+#include "channel.h"
+#include "migration.h"
+#include "rdma.h"
+#include "trace.h"
+#include <stdio.h>
+
+static struct RDMAOutgoingArgs {
+    InetSocketAddress *addr;
+} outgoing_args;
+
+static void rdma_outgoing_migration(QIOTask *task, gpointer opaque)
+{
+    MigrationState *s = opaque;
+    QIOChannelRDMA *rioc = QIO_CHANNEL_RDMA(qio_task_get_source(task));
+
+    migration_channel_connect(s, QIO_CHANNEL(rioc), outgoing_args.addr->host,
+                              NULL);
+    object_unref(OBJECT(rioc));
+}
+
+void rdma_start_outgoing_migration(MigrationState *s, InetSocketAddress *iaddr,
+                                   Error **errp)
+{
+    QIOChannelRDMA *rioc = qio_channel_rdma_new();
+
+    /* in case previous migration leaked it */
+    qapi_free_InetSocketAddress(outgoing_args.addr);
+    outgoing_args.addr = QAPI_CLONE(InetSocketAddress, iaddr);
+
+    qio_channel_set_name(QIO_CHANNEL(rioc), "migration-rdma-outgoing");
+    qio_channel_rdma_connect_async(rioc, iaddr, rdma_outgoing_migration, s,
+                                   NULL, NULL);
+}
+
+static void coroutine_fn rdma_accept_incoming_migration(void *opaque)
+{
+    QIOChannelRDMA *rioc = opaque;
+    QIOChannelRDMA *cioc;
+
+    while (!migration_has_all_channels()) {
+        cioc = qio_channel_rdma_accept(rioc, NULL);
+
+        qio_channel_set_name(QIO_CHANNEL(cioc), "migration-rdma-incoming");
+        migration_channel_process_incoming(QIO_CHANNEL(cioc));
+        object_unref(OBJECT(cioc));
+    }
+}
+
+void rdma_start_incoming_migration(InetSocketAddress *addr, Error **errp)
+{
+    QIOChannelRDMA *rioc = qio_channel_rdma_new();
+    MigrationIncomingState *mis = migration_incoming_get_current();
+    Coroutine *co;
+    int num = 1;
+
+    qio_channel_set_name(QIO_CHANNEL(rioc), "migration-rdma-listener");
+
+    if (qio_channel_rdma_listen_sync(rioc, addr, num, errp) < 0) {
+        object_unref(OBJECT(rioc));
+        return;
+    }
+
+    mis->transport_data = rioc;
+    mis->transport_cleanup = object_unref;
+
+    qio_channel_set_blocking(QIO_CHANNEL(rioc), false, NULL);
+    co = qemu_coroutine_create(rdma_accept_incoming_migration, rioc);
+    aio_co_schedule(qemu_get_current_aio_context(), co);
+}
diff --git a/migration/rdma.h b/migration/rdma.h
new file mode 100644
index 0000000000..4c3eb9a972
--- /dev/null
+++ b/migration/rdma.h
@@ -0,0 +1,24 @@
+/*
+ * QEMU live migration via RDMA
+ *
+ * Copyright (c) 2024 HUAWEI TECHNOLOGIES CO., LTD.
+ *
+ * Authors:
+ *  Jialin Wang <wangjialin23@huawei.com>
+ *  Gonglei <arei.gonglei@huawei.com>
+ *
+ * This work is licensed under the terms of the GNU GPL, version 2 or later.
+ * See the COPYING file in the top-level directory.
+ */
+
+#ifndef QEMU_MIGRATION_RDMA_H
+#define QEMU_MIGRATION_RDMA_H
+
+#include "qemu/sockets.h"
+
+void rdma_start_outgoing_migration(MigrationState *s, InetSocketAddress *addr,
+                                   Error **errp);
+
+void rdma_start_incoming_migration(InetSocketAddress *addr, Error **errp);
+
+#endif /* QEMU_MIGRATION_RDMA_H */
