diff --git a/qapi-schema.json b/qapi-schema.json
index ac9594d..f427e04 100644
--- a/qapi-schema.json
+++ b/qapi-schema.json
@@ -2876,6 +2876,21 @@
 { 'type': 'ChardevRingbuf', 'data': { '*size'  : 'int' } }
 
 ##
+# @ChardevIrc
+#
+# Configuration info for IRC chardevs.
+#
+# @addr:    IRC server
+# @nick:    Nick for qemu to use
+# @channel: Channel to join, or nick to query
+#
+# Since: 2.4
+##
+{ 'type': 'ChardevIrc', 'data': { 'addr'        : 'SocketAddress',
+                                  'nick'        : 'str',
+                                  'channel'     : 'str' } }
+
+##
 # @ChardevBackend:
 #
 # Configuration info for the new chardev backend.
@@ -2902,6 +2917,7 @@
                                        'spiceport' : 'ChardevSpicePort',
                                        'vc'     : 'ChardevVC',
                                        'ringbuf': 'ChardevRingbuf',
+                                       'irc'    : 'ChardevIrc',
                                        # next one is just for compatibility
                                        'memory' : 'ChardevRingbuf' } }
 
diff --git a/qemu-char.c b/qemu-char.c
index a405d76..a68fef3 100644
--- a/qemu-char.c
+++ b/qemu-char.c
@@ -3296,6 +3296,296 @@ char *qmp_ringbuf_read(const char *device, int64_t size,
     return data;
 }
 
+/*********************************************************/
+/* IRC chardev */
+
+typedef struct {
+    int fd;
+    bool query;
+    char *nick, *channel;
+
+    GIOChannel *chan;
+
+    uint8_t *sendbuf;
+    int sendbuf_idx, sendbuf_prefixlen;
+    bool send_line_skip;
+
+    uint8_t *recvbuf;
+    int recvbuf_idx;
+    bool recv_line_skip;
+} IrcCharDriverState;
+
+static ssize_t irc_send(IrcCharDriverState *irc, const void *buf, size_t len)
+{
+    return send_all(irc->fd, buf, len);
+}
+
+static ssize_t irc_recv(IrcCharDriverState *irc, void *buf, size_t len)
+{
+    return recv_all(irc->fd, buf, len, true);
+}
+
+static int irc_read_line(IrcCharDriverState *irc, char **buf)
+{
+    ssize_t ret;
+    int eol_idx = -1, i;
+
+    for (i = 0; i < irc->recvbuf_idx; i++) {
+        if (irc->recvbuf[i] == '\n' || irc->recvbuf[i] == '\r') {
+            break;
+        }
+    }
+
+    if (i < irc->recvbuf_idx) {
+        eol_idx = i;
+    }
+
+    while (eol_idx < 0) {
+        if (irc->recvbuf_idx >= 512) {
+            irc->recvbuf_idx = 0;
+            irc->recv_line_skip = true;
+        }
+
+        ret = irc_recv(irc, irc->recvbuf + irc->recvbuf_idx,
+                       512 - irc->recvbuf_idx);
+        if (ret < 0) {
+            irc->recvbuf_idx = 0;
+            return ret;
+        }
+
+        for (i = 0; i < ret; i++) {
+            if (irc->recvbuf[irc->recvbuf_idx + i] == '\n' ||
+                irc->recvbuf[irc->recvbuf_idx + i] == '\r')
+            {
+                break;
+            }
+        }
+        irc->recvbuf_idx += ret;
+
+        if (i < ret) {
+            eol_idx = irc->recvbuf_idx - ret + i;
+            break;
+        }
+    }
+
+    if (!irc->recv_line_skip) {
+        *buf = g_new(char, 512);
+        memcpy(*buf, irc->recvbuf, eol_idx);
+        (*buf)[eol_idx] = 0;
+    }
+
+    /* CRLF is the standard, but supporting LF only will not hurt */
+    if (irc->recvbuf[++eol_idx] == '\n') {
+        ++eol_idx;
+    }
+
+    memmove(irc->recvbuf, irc->recvbuf + eol_idx, 512 - eol_idx);
+    irc->recvbuf_idx -= eol_idx;
+    irc->recvbuf[irc->recvbuf_idx] = 0;
+
+    if (irc->recv_line_skip) {
+        irc->recv_line_skip = false;
+        return -EPROTO;
+    }
+    return 0;
+}
+
+static int irc_chr_write(CharDriverState *s, const uint8_t *buf, int len)
+{
+    IrcCharDriverState *irc = s->opaque;
+    static const int max_line_length = 384; /* 512 minus the prefix */
+    int i;
+
+    if (!len) {
+        return 0;
+    }
+
+    for (i = 0; i < len; i++) {
+        if (buf[i] == '\r') {
+            continue;
+        } else if (buf[i] == '\n') {
+            if (irc->sendbuf) {
+                if (!irc->send_line_skip) {
+                    irc->sendbuf[irc->sendbuf_idx++] = '\r';
+                    irc->sendbuf[irc->sendbuf_idx++] = '\n';
+
+                    /* Ignore errors */
+                    irc_send(irc, irc->sendbuf, irc->sendbuf_idx);
+                }
+
+                irc->sendbuf_idx = irc->sendbuf_prefixlen;
+                irc->send_line_skip = false;
+            }
+        } else if (iscntrl(buf[i])) {
+            /* IRC is no terminal, control characters do not make a whole lot
+             * of sense; just skip those lines */
+            irc->send_line_skip = true;
+        } else {
+            assert(irc->sendbuf_idx <= max_line_length);
+
+            if (irc->sendbuf_idx == max_line_length) {
+                int break_index;
+
+                for (break_index = max_line_length - 1; break_index > 0;
+                     break_index--)
+                {
+                    if (isspace(irc->sendbuf[break_index])) {
+                        break;
+                    }
+                }
+                if (break_index > 0) {
+                    irc->sendbuf[break_index] = 0;
+                } else {
+                    break_index = max_line_length;
+                }
+
+                if (!irc->send_line_skip) {
+                    char replaced = irc->sendbuf[break_index + 1];
+
+                    irc->sendbuf[break_index] = '\r';
+                    irc->sendbuf[break_index + 1] = '\n';
+
+                    irc_send(irc, irc->sendbuf, break_index + 2);
+
+                    irc->sendbuf[break_index + 1] = replaced;
+                }
+
+                memmove(&irc->sendbuf[irc->sendbuf_prefixlen],
+                        &irc->sendbuf[break_index + 1],
+                        max_line_length - break_index - 1);
+
+                irc->sendbuf_idx = irc->sendbuf_prefixlen +
+                                   max_line_length - break_index - 1;
+            }
+
+            irc->sendbuf[irc->sendbuf_idx++] = buf[i];
+        }
+    }
+
+    return len;
+}
+
+static int irc_chr_read_poll(void *opaque)
+{
+    CharDriverState *s = opaque;
+    return qemu_chr_be_can_write(s);
+}
+
+static gboolean irc_chr_read(GIOChannel *chan, GIOCondition cond, void *opaque)
+{
+    CharDriverState *s = opaque;
+    IrcCharDriverState *irc = s->opaque;
+    char *cmd, *src = NULL, *dest = NULL, *msg = NULL;
+    char *cmd_save = NULL, *line, real_src[128], real_dest[128], *buffer;
+    bool in_query;
+    int i, ret;
+
+    ret = irc_read_line(irc, &buffer);
+    if (ret < 0) {
+        return FALSE;
+    }
+    line = buffer;
+
+    cmd = strtok_r(line, " ", &cmd_save);
+    if (cmd && cmd[0] == ':') {
+        src = cmd + 1;
+        cmd = strtok_r(NULL, " ", &cmd_save);
+    }
+
+    if (!cmd) {
+        g_free(buffer);
+        return FALSE;
+    }
+
+    if (!strcmp(cmd, "PING")) {
+        char *pong = g_strdup_printf("PONG %s\r\n", cmd + strlen(cmd) + 1);
+        /* Ignore errors */
+        irc_send(irc, pong, strlen(pong));
+        g_free(pong);
+        return TRUE;
+    }
+
+    dest = strtok_r(NULL, " ", &cmd_save);
+    if (!dest || dest[0] == ':') {
+        g_free(buffer);
+        return FALSE;
+    }
+
+    msg = dest + strlen(dest) + 1;
+    if (msg[0] == ':') {
+        msg++;
+    } else {
+        msg = strtok_r(NULL, " ", &cmd_save);
+    }
+
+    for (i = 0; src[i] && src[i] != '!' && i < 127; i++) {
+        real_src[i] = src[i];
+    }
+    real_src[i] = 0;
+
+    for (i = 0; dest[i] && dest[i] != '!' && i < 127; i++) {
+        real_dest[i] = dest[i];
+    }
+    real_dest[i] = 0;
+
+    (void)real_src;
+
+    in_query = !strcmp(real_dest, irc->nick);
+
+    /* ignore CTCP */
+    if (!strcmp(cmd, "PRIVMSG") && msg[0] != 1 &&
+        (in_query || (!strncmp(msg, irc->nick, strlen(irc->nick)) &&
+                      !isalnum(msg[strlen(irc->nick)]))))
+    {
+        const char *start = msg;
+
+        if (!in_query) {
+            start = msg + strlen(irc->nick) + 1;
+            while (*start && isspace(*start)) {
+                start++;
+            }
+        }
+
+        qemu_chr_be_write(s, (uint8_t *)start, strlen(start));
+        qemu_chr_be_write(s, (uint8_t *)"\n", 1);
+    }
+
+    g_free(buffer);
+    return TRUE;
+}
+
+static GSource *irc_chr_add_watch(CharDriverState *s, GIOCondition cond)
+{
+    IrcCharDriverState *irc = s->opaque;
+    return g_io_create_watch(irc->chan, cond);
+}
+
+static void irc_chr_update_read_handler(CharDriverState *s)
+{
+    IrcCharDriverState *irc = s->opaque;
+    io_add_watch_poll(irc->chan, irc_chr_read_poll, irc_chr_read, s);
+}
+
+static void irc_destroy(IrcCharDriverState *irc)
+{
+    irc_send(irc, "QUIT\r\n", strlen("QUIT\r\n"));
+    g_io_channel_unref(irc->chan);
+    close(irc->fd);
+
+    g_free(irc->nick);
+    g_free(irc->channel);
+    g_free(irc->sendbuf);
+    g_free(irc->recvbuf);
+
+    g_free(irc);
+}
+
+static void irc_chr_close(CharDriverState *s)
+{
+    irc_destroy(s->opaque);
+    s->opaque = NULL;
+}
+
 QemuOpts *qemu_chr_parse_compat(const char *label, const char *filename)
 {
     char host[65], port[33], width[8], height[8];
@@ -3433,6 +3723,16 @@ QemuOpts *qemu_chr_parse_compat(const char *label, const char *filename)
         return opts;
     }
 
+    if (strstart(filename, "irc,", &p)) {
+        qemu_opts_do_parse(opts, p, NULL, &local_err);
+        if (local_err) {
+            error_report_err(local_err);
+            goto fail;
+        }
+        qemu_opt_set(opts, "backend", "irc", &error_abort);
+        return opts;
+    }
+
 fail:
     qemu_opts_del(opts);
     return NULL;
@@ -3634,6 +3934,40 @@ static void qemu_chr_parse_udp(QemuOpts *opts, ChardevBackend *backend,
     }
 }
 
+static void qemu_chr_parse_irc(QemuOpts *opts, ChardevBackend *backend,
+                               Error **errp)
+{
+    const char *host, *port, *nick, *channel;
+    SocketAddress *addr;
+
+    host    = qemu_opt_get(opts, "host");
+    port    = qemu_opt_get(opts, "port") ?: "6667";
+    nick    = qemu_opt_get(opts, "nick");
+    channel = qemu_opt_get(opts, "channel");
+
+    if (!host || !nick || !channel) {
+        error_setg(errp, "chardev: irc: Missing options");
+        return;
+    }
+
+    if (strlen(nick) > 64 || strlen(channel) > 64) {
+        error_setg(errp, "chardev: irc: Nick or channel too long");
+        return;
+    }
+
+    backend->irc = g_new0(ChardevIrc, 1);
+
+    addr = g_new0(SocketAddress, 1);
+    addr->kind = SOCKET_ADDRESS_KIND_INET;
+    addr->inet = g_new0(InetSocketAddress, 1);
+    addr->inet->host = g_strdup(host);
+    addr->inet->port = g_strdup(port);
+    backend->irc->addr = addr;
+
+    backend->irc->nick = g_strdup(nick);
+    backend->irc->channel = g_strdup(channel);
+}
+
 typedef struct CharDriver {
     const char *name;
     ChardevBackendKind kind;
@@ -3992,6 +4326,12 @@ QemuOptsList qemu_chardev_opts = {
         },{
             .name = "chardev",
             .type = QEMU_OPT_STRING,
+        },{
+            .name = "nick",
+            .type = QEMU_OPT_STRING,
+        },{
+            .name = "channel",
+            .type = QEMU_OPT_STRING,
         },
         { /* end of list */ }
     },
@@ -4206,6 +4546,154 @@ static CharDriverState *qmp_chardev_open_udp(ChardevUdp *udp,
     return qemu_chr_open_udp_fd(fd);
 }
 
+#ifdef CONFIG_GNUTLS
+static ssize_t push_all(gnutls_transport_ptr_t tptr,
+                        const void *buf, size_t len)
+{
+    return send_all((uintptr_t)tptr, buf, len);
+}
+
+static ssize_t pull_all(gnutls_transport_ptr_t tptr, void *buf, size_t len)
+{
+    return recv_all((uintptr_t)tptr, buf, len, true);
+}
+
+static int gnutls_cert_allow_all(gnutls_session_t session)
+{
+    return 0;
+}
+#endif
+
+static CharDriverState *qemu_chr_open_irc(ChardevIrc *irc, Error **errp)
+{
+    IrcCharDriverState *irc_cds;
+    CharDriverState *s;
+    Error *local_err = NULL;
+    GIOChannel *chan;
+    char *sendstr, *buffer = NULL;
+    bool joined = false, query;
+    int fd, ret;
+
+    fd = socket_connect(irc->addr, &local_err, NULL, NULL);
+    if (local_err) {
+        error_propagate(errp, local_err);
+        return NULL;
+    }
+
+#ifdef _WIN32
+    chan = g_io_channel_win32_new_socket(fd);
+#else
+    chan = g_io_channel_unix_new(fd);
+#endif
+
+    irc_cds = g_new0(IrcCharDriverState, 1);
+    irc_cds->fd      = fd;
+    irc_cds->nick    = g_strdup(irc->nick);
+    irc_cds->channel = g_strdup(irc->channel);
+
+    irc_cds->chan = chan;
+
+    irc_cds->sendbuf = g_malloc(512);
+    irc_cds->sendbuf_prefixlen = sprintf((char *)irc_cds->sendbuf,
+                                         "PRIVMSG %s :", irc_cds->channel);
+    irc_cds->sendbuf_idx = irc_cds->sendbuf_prefixlen;
+
+    irc_cds->recvbuf = g_malloc(1024);
+    irc_cds->recvbuf_idx = 0;
+
+    sendstr = g_strdup_printf("USER qemu qemu qemu qemu\r\nNICK %s\r\n",
+                              irc->nick);
+    ret = irc_send(irc_cds, sendstr, strlen(sendstr));
+    g_free(sendstr);
+    if (ret < 0) {
+        error_setg(errp, "Failed to send IRC USER+NICK");
+        goto fail;
+    }
+
+    query = irc->channel[0] != '#' && irc->channel[0] != '&' &&
+            irc->channel[0] != '+' && irc->channel[0] != '!';
+
+    while (!joined) {
+        const char *pars, *cmd, *src = NULL;
+        char *cmd_save = NULL;
+
+        ret = irc_read_line(irc_cds, &buffer);
+        if (ret < 0) {
+            continue;
+        }
+
+        cmd = strtok_r(buffer, " ", &cmd_save);
+        if (cmd && cmd[0] == ':') {
+            src = cmd + 1;
+            cmd = strtok_r(NULL, " ", &cmd_save);
+        }
+
+        if (!cmd) {
+            continue;
+        }
+
+        pars = cmd + strlen(cmd) + 1;
+
+        if (!strcmp(cmd, "PING")) {
+            sendstr = g_strdup_printf("PONG %s\r\n", pars);
+            ret = irc_send(irc_cds, sendstr, strlen(sendstr));
+            g_free(sendstr);
+            if (ret < 0) {
+                error_setg_errno(errp, errno, "Failed to send IRC PONG");
+                goto fail;
+            }
+        } else if (!strcmp(cmd, "376")) {
+            /* Ignore erros */
+            irc_send(irc_cds, "MODE qemu42 +B qemu42\r\n",
+                     strlen("MODE qemu42 +B qemu42\r\n"));
+
+            if (query) {
+                joined = true;
+            } else {
+                sendstr = g_strdup_printf("JOIN %s\r\n", irc->channel);
+                ret = irc_send(irc_cds, sendstr, strlen(sendstr));
+                g_free(sendstr);
+                if (ret < 0) {
+                    error_setg_errno(errp, errno, "Failed to send IRC JOIN");
+                    goto fail;
+                }
+            }
+        } else if (!strcmp(cmd, "JOIN")) {
+            char real_src[128];
+            int i;
+
+            for (i = 0; src[i] && src[i] != '!' && i < 127; i++) {
+                real_src[i] = src[i];
+            }
+            real_src[i] = 0;
+
+            if (!strcmp(real_src, irc->nick)) {
+                joined = true;
+            }
+        }
+
+        g_free(buffer);
+        buffer = NULL;
+    }
+
+    irc_cds->query = query;
+
+    s = qemu_chr_alloc();
+    s->chr_write = irc_chr_write;
+    s->chr_close = irc_chr_close;
+    s->chr_add_watch = irc_chr_add_watch;
+    s->chr_update_read_handler = irc_chr_update_read_handler;
+
+    s->opaque = irc_cds;
+
+    return s;
+
+
+fail:
+    irc_destroy(irc_cds);
+    return NULL;
+}
+
 ChardevReturn *qmp_chardev_add(const char *id, ChardevBackend *backend,
                                Error **errp)
 {
@@ -4289,6 +4777,9 @@ ChardevReturn *qmp_chardev_add(const char *id, ChardevBackend *backend,
     case CHARDEV_BACKEND_KIND_MEMORY:
         chr = qemu_chr_open_ringbuf(backend->ringbuf, errp);
         break;
+    case CHARDEV_BACKEND_KIND_IRC:
+        chr = qemu_chr_open_irc(backend->irc, errp);
+        break;
     default:
         error_setg(errp, "unknown chardev backend (%d)", backend->kind);
         break;
@@ -4366,6 +4857,7 @@ static void register_types(void)
     /* Bug-compatibility: */
     register_char_driver("memory", CHARDEV_BACKEND_KIND_MEMORY,
                          qemu_chr_parse_ringbuf);
+    register_char_driver("irc", CHARDEV_BACKEND_KIND_IRC, qemu_chr_parse_irc);
     /* this must be done after machine init, since we register FEs with muxes
      * as part of realize functions like serial_isa_realizefn when -nographic
      * is specified
