diff --git a/hw/ipmi/ipmi_host_extern.c b/hw/ipmi/ipmi_host_extern.c
new file mode 100644
index 0000000000..6b5d45d403
--- /dev/null
+++ b/hw/ipmi/ipmi_host_extern.c
@@ -0,0 +1,435 @@
+/*
+ * IPMI Host external connection
+ *
+ * Copyright 2020 Google LLC
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+/*
+ * This is designed to connect to a host QEMU VM that runs ipmi_bmc_extern.
+ */
+
+#include "qemu/osdep.h"
+#include "qemu/error-report.h"
+#include "qemu/module.h"
+#include "qapi/error.h"
+#include "qemu/timer.h"
+#include "chardev/char-fe.h"
+#include "hw/ipmi/ipmi.h"
+#include "hw/ipmi/ipmi_host.h"
+#include "hw/ipmi/ipmi_responder.h"
+#include "hw/qdev-properties.h"
+#include "migration/vmstate.h"
+
+#define VM_MSG_CHAR        0xA0 /* Marks end of message */
+#define VM_CMD_CHAR        0xA1 /* Marks end of a command */
+#define VM_ESCAPE_CHAR     0xAA /* Set bit 4 from the next byte to 0 */
+
+#define VM_PROTOCOL_VERSION        1
+#define VM_CMD_VERSION             0xff /* A version number byte follows */
+#define VM_CMD_RESET               0x04
+#define VM_CMD_CAPABILITIES        0x08
+
+#define TYPE_IPMI_HOST_EXTERN "ipmi-host-extern"
+#define IPMI_HOST_EXTERN(obj) OBJECT_CHECK(IPMIHostExtern, (obj), \
+                                        TYPE_IPMI_HOST_EXTERN)
+
+typedef struct IPMIHostExtern {
+    IPMIHost parent;
+    CharBackend chr;
+    struct QEMUTimer *extern_timer;
+
+    bool connected;
+    uint8_t capability;
+
+    unsigned char inbuf[MAX_IPMI_MSG_SIZE + 2];
+    unsigned int inpos;
+    bool in_escape;
+    bool in_too_many;
+    bool sending_cmd;
+
+    unsigned char outbuf[(MAX_IPMI_MSG_SIZE + 2) * 2 + 1];
+    unsigned int outpos;
+    unsigned int outlen;
+} IPMIHostExtern;
+
+static unsigned char
+ipmb_checksum(const unsigned char *data, int size, unsigned char start)
+{
+    unsigned char csum = start;
+
+    for (; size > 0; size--, data++) {
+            csum += *data;
+    }
+    return csum;
+}
+
+static void continue_send(IPMIHostExtern *ihe)
+{
+    int ret;
+
+    if (ihe->outlen == 0) {
+        return;
+    }
+    ret = qemu_chr_fe_write(&ihe->chr, ihe->outbuf + ihe->outpos,
+                            ihe->outlen - ihe->outpos);
+    if (ret > 0) {
+        ihe->outpos += ret;
+    }
+    if (ihe->outpos < ihe->outlen) {
+        /* Not fully transmitted, try again in a 10ms */
+        timer_mod_ns(ihe->extern_timer,
+                     qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + 10000000);
+    } else {
+        /* Sent */
+        ihe->outlen = 0;
+        ihe->outpos = 0;
+    }
+}
+
+static void extern_timeout(void *opaque)
+{
+    IPMIHostExtern *ihe = opaque;
+
+    if (ihe->connected) {
+        continue_send(ihe);
+    }
+}
+
+static void addchar(IPMIHostExtern *ihe, unsigned char ch)
+{
+    switch (ch) {
+    case VM_MSG_CHAR:
+    case VM_CMD_CHAR:
+    case VM_ESCAPE_CHAR:
+        /* Escape the special characters. */
+        ihe->outbuf[ihe->outlen++] = VM_ESCAPE_CHAR;
+        ch |= 0x10;
+        /* fall through */
+    default:
+        ihe->outbuf[ihe->outlen++] = ch;
+    }
+}
+
+static void send_version(IPMIHostExtern *ihe)
+{
+    addchar(ihe, VM_CMD_VERSION);
+    addchar(ihe, VM_PROTOCOL_VERSION);
+    ihe->outbuf[ihe->outlen++] = VM_CMD_CHAR;
+    continue_send(ihe);
+}
+
+/*
+ * Handle a command (typically IPMI response) from IPMI responder
+ * and send it out to the external host.
+ */
+static void ipmi_host_extern_handle_command(IPMIHost *h, uint8_t *cmd,
+        unsigned cmd_len, unsigned max_cmd_len, uint8_t msg_id)
+{
+    IPMIHostExtern *ihe = IPMI_HOST_EXTERN(h);
+    uint8_t err = 0, csum;
+    int i;
+
+    if (!ihe->connected) {
+        /* We are not connected to external host. Just do nothing. */
+        return;
+    }
+    addchar(ihe, msg_id);
+    /* If it's too short or it was truncated, return an error. */
+    if (cmd_len < 2) {
+        err = IPMI_CC_REQUEST_DATA_LENGTH_INVALID;
+    } else if ((cmd_len > max_cmd_len) || (cmd_len > MAX_IPMI_MSG_SIZE)) {
+        err = IPMI_CC_REQUEST_DATA_TRUNCATED;
+    }
+    if (err) {
+        /* Send out the error message */
+        unsigned char rsp[3];
+
+        rsp[0] = cmd[0] | 0x04;
+        rsp[1] = cmd[1];
+        rsp[2] = err;
+        for (i = 0; i < 3; ++i) {
+            addchar(ihe, rsp[i]);
+        }
+        csum = ipmb_checksum(&msg_id, 1, 0);
+        addchar(ihe, -ipmb_checksum(rsp, 3, csum));
+    } else {
+        for (i = 0; i < cmd_len; i++) {
+            addchar(ihe, cmd[i]);
+        }
+        csum = ipmb_checksum(&msg_id, 1, 0);
+        addchar(ihe, -ipmb_checksum(cmd, cmd_len, csum));
+    }
+
+    ihe->outbuf[ihe->outlen++] = VM_MSG_CHAR;
+
+    /* Start the transmit */
+    continue_send(ihe);
+}
+
+/*
+ * This function handles an IPMI message received from an external host by
+ * sending it to the IPMI responder class.
+ */
+static void handle_msg(IPMIHostExtern *ihe)
+{
+    IPMIResponderClass *k = IPMI_RESPONDER_GET_CLASS(ihe->parent.responder);
+    if (unlikely(ihe->in_escape)) {
+        ipmi_debug("msg escape not ended\n");
+        return;
+    }
+    if (unlikely(ihe->inpos < 4)) {
+        ipmi_debug("msg too short\n");
+        return;
+    }
+    if (unlikely(ihe->in_too_many)) {
+        ihe->inbuf[3] = IPMI_CC_REQUEST_DATA_TRUNCATED;
+        ihe->inpos = 4;
+    } else if (unlikely(ipmb_checksum(ihe->inbuf, ihe->inpos, 0) != 0)) {
+        ipmi_debug("msg checksum failure\n");
+        return;
+    } else {
+        ihe->inpos--; /* Remove checkum */
+    }
+
+    k->handle_req(ihe->parent.responder, ihe->inbuf[0], ihe->inbuf + 1,
+            ihe->inpos - 1);
+}
+
+/* This function handles a control command from the host. */
+static void handle_command(IPMIHostExtern *ihe)
+{
+    uint8_t cmd;
+
+    if (unlikely(ihe->in_too_many)) {
+        ipmi_debug("cmd in too many\n");
+        return;
+    }
+
+    if (unlikely(ihe->in_escape)) {
+        ipmi_debug("cmd ends with escape character\n");
+        return;
+    }
+
+    if (unlikely(ihe->inpos < 1)) {
+        ipmi_debug("empty command.\n");
+        return;
+    }
+
+    cmd = ihe->inbuf[0];
+    switch (cmd) {
+    case VM_CMD_VERSION:
+        /* The host informs us the protocol version. */
+        if (unlikely(ihe->inpos < 2)) {
+            ipmi_debug("Host cmd version truncated.\n");
+            break;
+        }
+        if (unlikely(ihe->inbuf[1] != VM_PROTOCOL_VERSION)) {
+            ipmi_debug("Host protocol version %u is different from our version"
+                    " %u\n", ihe->inbuf[1], VM_PROTOCOL_VERSION);
+        }
+        break;
+    case VM_CMD_RESET:
+        /* The host tells us a reset has happened. */
+        break;
+    case VM_CMD_CAPABILITIES:
+        /* The host tells us its capability. */
+        if (unlikely(ihe->inpos < 2)) {
+            ipmi_debug("Host cmd capability truncated.\n");
+            break;
+        }
+        ihe->capability = ihe->inbuf[1];
+        break;
+    default:
+        /* The host shouldn't send us this command. Just ignore if they do. */
+        ipmi_debug("Host cmd type %02x is invalid.\n", cmd);
+    }
+}
+
+/* Clear the state of ipmi-host-extern. Happens at the end of a message. */
+static void clear_state(IPMIHostExtern *ihe)
+{
+    ihe->in_escape = false;
+    ihe->in_too_many = false;
+    ihe->inpos = 0;
+}
+
+/* We always welcome an incoming request. */
+static int can_receive(void *opaque)
+{
+    return 1;
+}
+
+/*
+ * This function mirrors ipmi-bmc-extern. It handles an incoming character
+ * sequence and translates it into IPMI message.
+ */
+static void receive(void *opaque, const uint8_t *buf, int size)
+{
+    IPMIHostExtern *ihe = opaque;
+    int i;
+
+    for (i = 0; i < size; ++i) {
+        uint8_t ch = buf[i];
+
+        switch (ch) {
+        case VM_MSG_CHAR:
+            /* The preceding characters are an IPMI message. */
+            handle_msg(ihe);
+            clear_state(ihe);
+            break;
+
+        case VM_CMD_CHAR:
+            /* The preceding characters are a control command. */
+            handle_command(ihe);
+            clear_state(ihe);
+            break;
+
+        case VM_ESCAPE_CHAR:
+            ihe->in_escape = true;
+            break;
+
+        default:
+            if (ihe->in_escape) {
+                ch &= ~0x10;
+                ihe->in_escape = false;
+            }
+            if (ihe->in_too_many) {
+                break;
+            }
+            if (ihe->inpos >= ARRAY_SIZE(ihe->inbuf)) {
+                ihe->in_too_many = true;
+                break;
+            }
+            ihe->inbuf[ihe->inpos++] = ch;
+            break;
+        }
+    }
+    return;
+}
+
+static void chr_event(void *opaque, QEMUChrEvent event)
+{
+    IPMIHostExtern *ihe = opaque;
+
+    switch (event) {
+    case CHR_EVENT_OPENED:
+        ihe->connected = true;
+        clear_state(ihe);
+        send_version(ihe);
+        break;
+
+    case CHR_EVENT_CLOSED:
+        ihe->connected = false;
+        break;
+
+    case CHR_EVENT_BREAK:
+    case CHR_EVENT_MUX_IN:
+    case CHR_EVENT_MUX_OUT:
+        /* Ignore */
+        break;
+
+    default:
+        g_assert_not_reached();
+    }
+}
+
+static void ipmi_host_extern_responder_check(const Object *obj,
+        const char *name, Object *val, Error **errp)
+{
+    IPMIHostExtern *ihe = IPMI_HOST_EXTERN(obj);
+
+    if (ihe->parent.responder) {
+        error_setg(errp, "IPMI host already has a responder");
+    }
+}
+
+static void ipmi_host_extern_realize(DeviceState *dev, Error **errp)
+{
+    IPMIHostExtern *ihe = IPMI_HOST_EXTERN(dev);
+    IPMIResponderClass *rk;
+
+    if (!qemu_chr_fe_backend_connected(&ihe->chr)) {
+        error_setg(errp, "IPMI external host requires chardev attribute");
+        return;
+    }
+
+    qemu_chr_fe_set_handlers(&ihe->chr, can_receive, receive,
+                             chr_event, NULL, ihe, NULL, true);
+
+    if (ihe->parent.responder == NULL) {
+        error_setg(errp, "IPMI host requires responder attribute");
+        return;
+    }
+    rk = IPMI_RESPONDER_GET_CLASS(ihe->parent.responder);
+    rk->set_host(ihe->parent.responder, &ihe->parent);
+}
+
+static const VMStateDescription vmstate_ipmi_host_extern = {
+    .name = TYPE_IPMI_HOST_EXTERN,
+    .version_id = 0,
+    .minimum_version_id = 0,
+    .fields      = (VMStateField[]) {
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void ipmi_host_extern_init(Object *obj)
+{
+    IPMIHostExtern *ihe = IPMI_HOST_EXTERN(obj);
+
+    object_property_add_link(OBJECT(ihe), "responder", TYPE_IPMI_RESPONDER,
+            (Object **)(&ihe->parent.responder),
+            ipmi_host_extern_responder_check, OBJ_PROP_LINK_STRONG);
+    ihe->extern_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, extern_timeout, ihe);
+    vmstate_register(NULL, 0, &vmstate_ipmi_host_extern, ihe);
+}
+
+static void ipmi_host_extern_finalize(Object *obj)
+{
+    IPMIHostExtern *ihe = IPMI_HOST_EXTERN(obj);
+
+    timer_del(ihe->extern_timer);
+    timer_free(ihe->extern_timer);
+}
+
+static Property ipmi_host_extern_properties[] = {
+    DEFINE_PROP_CHR("chardev", IPMIHostExtern, chr),
+    DEFINE_PROP_END_OF_LIST(),
+};
+
+static void ipmi_host_extern_class_init(ObjectClass *oc, void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(oc);
+    IPMIHostClass *bk = IPMI_HOST_CLASS(oc);
+
+    bk->handle_command = ipmi_host_extern_handle_command;
+    dc->hotpluggable = false;
+    dc->realize = ipmi_host_extern_realize;
+    device_class_set_props(dc, ipmi_host_extern_properties);
+}
+
+static const TypeInfo ipmi_host_extern_type = {
+    .name          = TYPE_IPMI_HOST_EXTERN,
+    .parent        = TYPE_IPMI_HOST,
+    .instance_size = sizeof(IPMIHostExtern),
+    .instance_init = ipmi_host_extern_init,
+    .instance_finalize = ipmi_host_extern_finalize,
+    .class_init    = ipmi_host_extern_class_init,
+ };
+
+static void ipmi_host_extern_register_types(void)
+{
+    type_register_static(&ipmi_host_extern_type);
+}
+
+type_init(ipmi_host_extern_register_types)
diff --git a/hw/ipmi/meson.build b/hw/ipmi/meson.build
index 1261489fbd..8352913288 100644
--- a/hw/ipmi/meson.build
+++ b/hw/ipmi/meson.build
@@ -8,6 +8,7 @@ ipmi_ss.add(when: 'CONFIG_ISA_IPMI_BT', if_true: files('isa_ipmi_bt.c'))
 ipmi_ss.add(when: 'CONFIG_PCI_IPMI_BT', if_true: files('pci_ipmi_bt.c'))
 ipmi_ss.add(when: 'CONFIG_IPMI_SSIF', if_true: files('smbus_ipmi.c'))
 ipmi_ss.add(when: 'CONFIG_IPMI_HOST', if_true: files('ipmi_host.c'))
+ipmi_ss.add(when: 'CONFIG_IPMI_HOST', if_true: files('ipmi_host_extern.c'))
 ipmi_ss.add(when: 'CONFIG_NPCM7XX', if_true: files('npcm7xx_kcs.c'))
 
 softmmu_ss.add_all(when: 'CONFIG_IPMI', if_true: ipmi_ss)
