Message ID | 20220620101828.518865-5-lvivier@redhat.com (mailing list archive) |
---|---|
State | New, archived |
Headers | show |
Series | qapi: net: add unix socket type support to netdev backend | expand |
Laurent Vivier <lvivier@redhat.com> writes: > Copied from socket netdev file and modified to use SocketAddress > to be able to introduce new features like unix socket. > > "udp" and "mcast" are squashed into dgram netdev, multicast is detected > according to the IP address type. > "listen" and "connect" modes are managed by stream netdev. An optional > parameter "server" defines the mode (server by default) > > Signed-off-by: Laurent Vivier <lvivier@redhat.com> > Reviewed-by: Stefano Brivio <sbrivio@redhat.com> > --- > hmp-commands.hx | 2 +- > net/clients.h | 6 + > net/dgram.c | 630 ++++++++++++++++++++++++++++++++++++++++++++++++ > net/hub.c | 2 + > net/meson.build | 2 + > net/net.c | 19 +- > net/stream.c | 425 ++++++++++++++++++++++++++++++++ > qapi/net.json | 40 ++- > qemu-options.hx | 12 + > 9 files changed, 1133 insertions(+), 5 deletions(-) > create mode 100644 net/dgram.c > create mode 100644 net/stream.c > > diff --git a/hmp-commands.hx b/hmp-commands.hx > index 564f1de364df..2938d13725cc 100644 > --- a/hmp-commands.hx > +++ b/hmp-commands.hx > @@ -1269,7 +1269,7 @@ ERST > { > .name = "netdev_add", > .args_type = "netdev:O", > - .params = "[user|tap|socket|vde|bridge|hubport|netmap|vhost-user" > + .params = "[user|tap|socket|stream|dgram|vde|bridge|hubport|netmap|vhost-user" > #ifdef CONFIG_VMNET > "|vmnet-host|vmnet-shared|vmnet-bridged" > #endif > diff --git a/net/clients.h b/net/clients.h > index c9157789f2ce..ed8bdfff1e7c 100644 > --- a/net/clients.h > +++ b/net/clients.h > @@ -40,6 +40,12 @@ int net_init_hubport(const Netdev *netdev, const char *name, > int net_init_socket(const Netdev *netdev, const char *name, > NetClientState *peer, Error **errp); > > +int net_init_stream(const Netdev *netdev, const char *name, > + NetClientState *peer, Error **errp); > + > +int net_init_dgram(const Netdev *netdev, const char *name, > + NetClientState *peer, Error **errp); > + > int net_init_tap(const Netdev *netdev, const char *name, > NetClientState *peer, Error **errp); > > diff --git a/net/dgram.c b/net/dgram.c > new file mode 100644 > index 000000000000..aa4240501ed0 > --- /dev/null > +++ b/net/dgram.c > @@ -0,0 +1,630 @@ > +/* > + * QEMU System Emulator > + * > + * Copyright (c) 2003-2008 Fabrice Bellard > + * > + * Permission is hereby granted, free of charge, to any person obtaining a copy > + * of this software and associated documentation files (the "Software"), to deal > + * in the Software without restriction, including without limitation the rights > + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell > + * copies of the Software, and to permit persons to whom the Software is > + * furnished to do so, subject to the following conditions: > + * > + * The above copyright notice and this permission notice shall be included in > + * all copies or substantial portions of the Software. > + * > + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR > + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, > + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL > + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER > + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, > + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN > + * THE SOFTWARE. > + */ Blank line here, please. > +#include "qemu/osdep.h" > + > +#include "net/net.h" > +#include "clients.h" > +#include "monitor/monitor.h" > +#include "qapi/error.h" > +#include "qemu/error-report.h" > +#include "qemu/option.h" > +#include "qemu/sockets.h" > +#include "qemu/iov.h" > +#include "qemu/main-loop.h" > +#include "qemu/cutils.h" > + > +typedef struct NetDgramState { > + NetClientState nc; > + int listen_fd; > + int fd; > + SocketReadState rs; > + /* contains inet host and port destination iff connectionless (SOCK_DGRAM) */ > + struct sockaddr_in dgram_dst; > + IOHandler *send_fn; /* differs between SOCK_STREAM/SOCK_DGRAM */ > + bool read_poll; /* waiting to receive data? */ > + bool write_poll; /* waiting to transmit data? */ > +} NetDgramState; > + > +static void net_dgram_accept(void *opaque); > +static void net_dgram_writable(void *opaque); > + > +static void net_dgram_update_fd_handler(NetDgramState *s) > +{ > + qemu_set_fd_handler(s->fd, > + s->read_poll ? s->send_fn : NULL, > + s->write_poll ? net_dgram_writable : NULL, > + s); > +} > + > +static void net_dgram_read_poll(NetDgramState *s, bool enable) > +{ > + s->read_poll = enable; > + net_dgram_update_fd_handler(s); > +} > + > +static void net_dgram_write_poll(NetDgramState *s, bool enable) > +{ > + s->write_poll = enable; > + net_dgram_update_fd_handler(s); > +} > + > +static void net_dgram_writable(void *opaque) > +{ > + NetDgramState *s = opaque; > + > + net_dgram_write_poll(s, false); > + > + qemu_flush_queued_packets(&s->nc); > +} > + > +static ssize_t net_dgram_receive_dgram(NetClientState *nc, > + const uint8_t *buf, size_t size) > +{ > + NetDgramState *s = DO_UPCAST(NetDgramState, nc, nc); > + ssize_t ret; > + > + do { > + if (s->dgram_dst.sin_family != AF_UNIX) { > + ret = sendto(s->fd, buf, size, 0, > + (struct sockaddr *)&s->dgram_dst, > + sizeof(s->dgram_dst)); > + } else { > + ret = send(s->fd, buf, size, 0); > + } > + } while (ret == -1 && errno == EINTR); > + > + if (ret == -1 && errno == EAGAIN) { > + net_dgram_write_poll(s, true); > + return 0; > + } > + return ret; > +} > + > +static void net_dgram_send_completed(NetClientState *nc, ssize_t len) > +{ > + NetDgramState *s = DO_UPCAST(NetDgramState, nc, nc); > + > + if (!s->read_poll) { > + net_dgram_read_poll(s, true); > + } > +} > + > +static void net_dgram_rs_finalize(SocketReadState *rs) > +{ > + NetDgramState *s = container_of(rs, NetDgramState, rs); > + > + if (qemu_send_packet_async(&s->nc, rs->buf, > + rs->packet_len, > + net_dgram_send_completed) == 0) { > + net_dgram_read_poll(s, false); > + } > +} > + > +static void net_dgram_send(void *opaque) > +{ > + NetDgramState *s = opaque; > + int size; > + int ret; > + uint8_t buf1[NET_BUFSIZE]; > + const uint8_t *buf; > + > + size = recv(s->fd, buf1, sizeof(buf1), 0); > + if (size < 0) { > + if (errno != EWOULDBLOCK) { > + goto eoc; > + } > + } else if (size == 0) { > + /* end of connection */ > + eoc: > + net_dgram_read_poll(s, false); > + net_dgram_write_poll(s, false); > + if (s->listen_fd != -1) { > + qemu_set_fd_handler(s->listen_fd, net_dgram_accept, NULL, s); > + } > + closesocket(s->fd); > + > + s->fd = -1; > + net_socket_rs_init(&s->rs, net_dgram_rs_finalize, false); > + s->nc.link_down = true; > + memset(s->nc.info_str, 0, sizeof(s->nc.info_str)); > + > + return; > + } > + buf = buf1; > + > + ret = net_fill_rstate(&s->rs, buf, size); > + > + if (ret == -1) { > + goto eoc; > + } > +} > + > +static void net_dgram_send_dgram(void *opaque) > +{ > + NetDgramState *s = opaque; > + int size; > + > + size = recv(s->fd, s->rs.buf, sizeof(s->rs.buf), 0); > + if (size < 0) { > + return; > + } > + if (size == 0) { > + /* end of connection */ > + net_dgram_read_poll(s, false); > + net_dgram_write_poll(s, false); > + return; > + } > + if (qemu_send_packet_async(&s->nc, s->rs.buf, size, > + net_dgram_send_completed) == 0) { > + net_dgram_read_poll(s, false); > + } > +} > + > +static int net_dgram_mcast_create(struct sockaddr_in *mcastaddr, > + struct in_addr *localaddr, > + Error **errp) > +{ > + struct ip_mreq imr; > + int fd; > + int val, ret; > +#ifdef __OpenBSD__ > + unsigned char loop; > +#else > + int loop; > +#endif > + > + if (!IN_MULTICAST(ntohl(mcastaddr->sin_addr.s_addr))) { > + error_setg(errp, "specified mcastaddr %s (0x%08x) " > + "does not contain a multicast address", > + inet_ntoa(mcastaddr->sin_addr), > + (int)ntohl(mcastaddr->sin_addr.s_addr)); > + return -1; > + } > + > + fd = qemu_socket(PF_INET, SOCK_DGRAM, 0); > + if (fd < 0) { > + error_setg_errno(errp, errno, "can't create datagram socket"); > + return -1; > + } > + > + /* > + * Allow multiple sockets to bind the same multicast ip and port by setting > + * SO_REUSEADDR. This is the only situation where SO_REUSEADDR should be set > + * on windows. Use socket_set_fast_reuse otherwise as it sets SO_REUSEADDR > + * only on posix systems. > + */ > + val = 1; > + ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); > + if (ret < 0) { > + error_setg_errno(errp, errno, "can't set socket option SO_REUSEADDR"); > + goto fail; > + } > + > + ret = bind(fd, (struct sockaddr *)mcastaddr, sizeof(*mcastaddr)); > + if (ret < 0) { > + error_setg_errno(errp, errno, "can't bind ip=%s to socket", > + inet_ntoa(mcastaddr->sin_addr)); > + goto fail; > + } > + > + /* Add host to multicast group */ > + imr.imr_multiaddr = mcastaddr->sin_addr; > + if (localaddr) { > + imr.imr_interface = *localaddr; > + } else { > + imr.imr_interface.s_addr = htonl(INADDR_ANY); > + } > + > + ret = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, > + &imr, sizeof(struct ip_mreq)); > + if (ret < 0) { > + error_setg_errno(errp, errno, > + "can't add socket to multicast group %s", > + inet_ntoa(imr.imr_multiaddr)); > + goto fail; > + } > + > + /* Force mcast msgs to loopback (eg. several QEMUs in same host */ > + loop = 1; > + ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, > + &loop, sizeof(loop)); > + if (ret < 0) { > + error_setg_errno(errp, errno, > + "can't force multicast message to loopback"); > + goto fail; > + } > + > + /* If a bind address is given, only send packets from that address */ > + if (localaddr != NULL) { > + ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, > + localaddr, sizeof(*localaddr)); > + if (ret < 0) { > + error_setg_errno(errp, errno, > + "can't set the default network send interface"); > + goto fail; > + } > + } > + > + qemu_socket_set_nonblock(fd); > + return fd; > +fail: > + if (fd >= 0) { > + closesocket(fd); > + } > + return -1; > +} > + > +static void net_dgram_cleanup(NetClientState *nc) > +{ > + NetDgramState *s = DO_UPCAST(NetDgramState, nc, nc); > + if (s->fd != -1) { > + net_dgram_read_poll(s, false); > + net_dgram_write_poll(s, false); > + close(s->fd); > + s->fd = -1; > + } > + if (s->listen_fd != -1) { > + qemu_set_fd_handler(s->listen_fd, NULL, NULL, NULL); > + closesocket(s->listen_fd); > + s->listen_fd = -1; > + } > +} > + > +static NetClientInfo net_dgram_socket_info = { > + .type = NET_CLIENT_DRIVER_DGRAM, > + .size = sizeof(NetDgramState), > + .receive = net_dgram_receive_dgram, > + .cleanup = net_dgram_cleanup, > +}; > + > +static NetDgramState *net_dgram_fd_init_dgram(NetClientState *peer, > + const char *model, > + const char *name, > + int fd, int is_fd, > + SocketAddress *mcast, > + Error **errp) > +{ > + struct sockaddr_in saddr; > + int newfd; > + NetClientState *nc; > + NetDgramState *s; > + SocketAddress *sa; > + SocketAddressType sa_type; > + > + sa = socket_local_address(fd, errp); > + if (!sa) { > + return NULL; > + } > + sa_type = sa->type; > + qapi_free_SocketAddress(sa); > + > + /* > + * fd passed: multicast: "learn" dgram_dst address from bound address and > + * save it. Because this may be "shared" socket from a "master" process, > + * datagrams would be recv() by ONLY ONE process: we must "clone" this > + * dgram socket --jjo > + */ > + > + if (is_fd && mcast != NULL) { > + if (convert_host_port(&saddr, mcast->u.inet.host, > + mcast->u.inet.port, errp) < 0) { > + goto err; > + } > + /* must be bound */ > + if (saddr.sin_addr.s_addr == 0) { > + error_setg(errp, "can't setup multicast destination address"); > + goto err; > + } > + /* clone dgram socket */ > + newfd = net_dgram_mcast_create(&saddr, NULL, errp); > + if (newfd < 0) { > + goto err; > + } > + /* clone newfd to fd, close newfd */ > + dup2(newfd, fd); > + close(newfd); > + > + } > + > + nc = qemu_new_net_client(&net_dgram_socket_info, peer, model, name); > + > + s = DO_UPCAST(NetDgramState, nc, nc); > + > + s->fd = fd; > + s->listen_fd = -1; > + s->send_fn = net_dgram_send_dgram; > + net_socket_rs_init(&s->rs, net_dgram_rs_finalize, false); > + net_dgram_read_poll(s, true); > + > + /* mcast: save bound address as dst */ > + if (is_fd && mcast != NULL) { > + s->dgram_dst = saddr; > + snprintf(nc->info_str, sizeof(nc->info_str), > + "fd=%d (cloned mcast=%s:%d)", > + fd, inet_ntoa(saddr.sin_addr), ntohs(saddr.sin_port)); > + } else { > + if (sa_type == SOCKET_ADDRESS_TYPE_UNIX) { > + s->dgram_dst.sin_family = AF_UNIX; > + } > + > + snprintf(nc->info_str, sizeof(nc->info_str), "fd=%d %s", fd, > + SocketAddressType_str(sa_type)); > + } > + > + return s; > + > +err: > + closesocket(fd); > + return NULL; > +} > + > +static void net_dgram_connect(void *opaque) > +{ > + NetDgramState *s = opaque; > + s->send_fn = net_dgram_send; > + net_dgram_read_poll(s, true); > +} > + > +static void net_dgram_accept(void *opaque) > +{ > + NetDgramState *s = opaque; > + struct sockaddr_in saddr; > + socklen_t len; > + int fd; > + > + for (;;) { > + len = sizeof(saddr); > + fd = qemu_accept(s->listen_fd, (struct sockaddr *)&saddr, &len); > + if (fd < 0 && errno != EINTR) { > + return; > + } else if (fd >= 0) { > + qemu_set_fd_handler(s->listen_fd, NULL, NULL, NULL); > + break; > + } > + } > + > + s->fd = fd; > + s->nc.link_down = false; > + net_dgram_connect(s); > + snprintf(s->nc.info_str, sizeof(s->nc.info_str), "connection from %s:%d", > + inet_ntoa(saddr.sin_addr), ntohs(saddr.sin_port)); > +} > + > +static int net_dgram_mcast_init(NetClientState *peer, > + const char *model, > + const char *name, > + SocketAddress *remote, > + SocketAddress *local, > + Error **errp) > +{ > + NetDgramState *s; > + int fd, ret; > + struct sockaddr_in saddr; > + > + if (remote->type != SOCKET_ADDRESS_TYPE_INET) { > + error_setg(errp, "multicast only support inet type"); > + return -1; > + } > + > + if (convert_host_port(&saddr, remote->u.inet.host, remote->u.inet.port, > + errp) < 0) { > + return -1; > + } > + > + if (!local) { > + fd = net_dgram_mcast_create(&saddr, NULL, errp); > + if (fd < 0) { > + return -1; > + } > + } else { > + switch (local->type) { > + case SOCKET_ADDRESS_TYPE_INET: { > + struct in_addr localaddr; > + > + if (inet_aton(local->u.inet.host, &localaddr) == 0) { > + error_setg(errp, "localaddr '%s' is not a valid IPv4 address", > + local->u.inet.host); > + return -1; > + } > + > + fd = net_dgram_mcast_create(&saddr, &localaddr, errp); > + if (fd < 0) { > + return -1; > + } > + break; > + } > + case SOCKET_ADDRESS_TYPE_FD: > + fd = monitor_fd_param(monitor_cur(), local->u.fd.str, errp); > + if (fd == -1) { > + return -1; > + } > + ret = qemu_socket_try_set_nonblock(fd); > + if (ret < 0) { > + error_setg_errno(errp, -ret, "%s: Can't use file descriptor %d", > + name, fd); > + return -1; > + } > + break; > + default: > + error_setg(errp, "only support inet or fd type for local"); > + return -1; > + } > + } > + > + s = net_dgram_fd_init_dgram(peer, model, name, fd, > + local->type == SOCKET_ADDRESS_TYPE_FD, > + remote, errp); > + if (!s) { > + return -1; > + } > + > + s->dgram_dst = saddr; > + > + snprintf(s->nc.info_str, sizeof(s->nc.info_str), "mcast=%s:%d", > + inet_ntoa(saddr.sin_addr), ntohs(saddr.sin_port)); > + return 0; > + > +} > + > +static int net_dgram_udp_init(NetClientState *peer, > + const char *model, > + const char *name, > + SocketAddress *remote, > + SocketAddress *local, > + Error **errp) > +{ > + NetDgramState *s; > + int fd, ret; > + struct sockaddr_in raddr_in; > + gchar *info_str; > + > + if (remote) { > + if (local->type == SOCKET_ADDRESS_TYPE_FD) { > + error_setg(errp, "don't set remote with local.fd"); > + return -1; > + } > + if (remote->type != local->type) { > + error_setg(errp, "remote and local types must be the same"); > + return -1; > + } > + } else { > + if (local->type != SOCKET_ADDRESS_TYPE_FD) { > + error_setg(errp, "type=inet requires remote parameter"); > + return -1; > + } > + } > + > + switch (local->type) { > + case SOCKET_ADDRESS_TYPE_INET: { > + struct sockaddr_in laddr_in; > + > + if (convert_host_port(&laddr_in, local->u.inet.host, local->u.inet.port, > + errp) < 0) { > + return -1; > + } > + > + if (convert_host_port(&raddr_in, remote->u.inet.host, > + remote->u.inet.port, errp) < 0) { > + return -1; > + } > + > + fd = qemu_socket(PF_INET, SOCK_DGRAM, 0); > + if (fd < 0) { > + error_setg_errno(errp, errno, "can't create datagram socket"); > + return -1; > + } > + > + ret = socket_set_fast_reuse(fd); > + if (ret < 0) { > + error_setg_errno(errp, errno, > + "can't set socket option SO_REUSEADDR"); > + closesocket(fd); > + return -1; > + } > + ret = bind(fd, (struct sockaddr *)&laddr_in, sizeof(laddr_in)); > + if (ret < 0) { > + error_setg_errno(errp, errno, "can't bind ip=%s to socket", > + inet_ntoa(laddr_in.sin_addr)); > + closesocket(fd); > + return -1; > + } > + qemu_socket_set_nonblock(fd); > + > + info_str = g_strdup_printf("udp=%s:%d/%s:%d", > + inet_ntoa(laddr_in.sin_addr), ntohs(laddr_in.sin_port), > + inet_ntoa(raddr_in.sin_addr), ntohs(raddr_in.sin_port)); > + > + break; > + } > + case SOCKET_ADDRESS_TYPE_FD: > + fd = monitor_fd_param(monitor_cur(), local->u.fd.str, errp); > + if (fd == -1) { > + return -1; > + } > + ret = qemu_socket_try_set_nonblock(fd); > + if (ret < 0) { > + error_setg_errno(errp, -ret, "%s: Can't use file descriptor %d", > + name, fd); > + return -1; > + } > + break; > + default: > + error_setg(errp, "only support inet or fd type for local"); > + return -1; > + } > + > + s = net_dgram_fd_init_dgram(peer, model, name, fd, 0, NULL, errp); > + if (!s) { > + return -1; > + } > + > + if (remote) { > + s->dgram_dst = raddr_in; > + > + pstrcpy(s->nc.info_str, sizeof(s->nc.info_str), info_str); > + g_free(info_str); > + } > + return 0; > +} > + > +static int net_dgram_init(NetClientState *peer, > + const char *model, > + const char *name, > + SocketAddress *remote, > + SocketAddress *local, > + Error **errp) > +{ > + /* detect multicast address */ > + if (remote && remote->type == SOCKET_ADDRESS_TYPE_INET) { > + struct sockaddr_in mcastaddr; > + > + if (convert_host_port(&mcastaddr, remote->u.inet.host, > + remote->u.inet.port, errp) < 0) { > + return -1; > + } > + > + if (IN_MULTICAST(ntohl(mcastaddr.sin_addr.s_addr))) { > + return net_dgram_mcast_init(peer, model, name, remote, local, > + errp); > + } > + } > + /* unicast address */ > + if (!local) { > + error_setg(errp, "dgram requires local= parameter"); > + return -1; > + } > + return net_dgram_udp_init(peer, model, name, remote, local, errp); > +} > + > +int net_init_dgram(const Netdev *netdev, const char *name, > + NetClientState *peer, Error **errp) > +{ > + const NetdevDgramOptions *sock; > + > + assert(netdev->type == NET_CLIENT_DRIVER_DGRAM); > + sock = &netdev->u.dgram; > + > + return net_dgram_init(peer, "dgram", name, sock->remote, sock->local, > + errp); > +} > diff --git a/net/hub.c b/net/hub.c > index 1375738bf121..67ca53485638 100644 > --- a/net/hub.c > +++ b/net/hub.c > @@ -313,6 +313,8 @@ void net_hub_check_clients(void) > case NET_CLIENT_DRIVER_USER: > case NET_CLIENT_DRIVER_TAP: > case NET_CLIENT_DRIVER_SOCKET: > + case NET_CLIENT_DRIVER_STREAM: > + case NET_CLIENT_DRIVER_DGRAM: > case NET_CLIENT_DRIVER_VDE: > case NET_CLIENT_DRIVER_VHOST_USER: > has_host_dev = 1; > diff --git a/net/meson.build b/net/meson.build > index 754e2d1d405c..896d86d43914 100644 > --- a/net/meson.build > +++ b/net/meson.build > @@ -13,6 +13,8 @@ softmmu_ss.add(files( > 'net.c', > 'queue.c', > 'socket.c', > + 'stream.c', > + 'dgram.c', > 'util.c', > )) > > diff --git a/net/net.c b/net/net.c > index c337d3d753fe..440957b272ee 100644 > --- a/net/net.c > +++ b/net/net.c > @@ -48,6 +48,7 @@ > #include "qemu/qemu-print.h" > #include "qemu/main-loop.h" > #include "qemu/option.h" > +#include "qemu/keyval.h" > #include "qapi/error.h" > #include "qapi/opts-visitor.h" > #include "sysemu/runstate.h" > @@ -1014,6 +1015,8 @@ static int (* const net_client_init_fun[NET_CLIENT_DRIVER__MAX])( > #endif > [NET_CLIENT_DRIVER_TAP] = net_init_tap, > [NET_CLIENT_DRIVER_SOCKET] = net_init_socket, > + [NET_CLIENT_DRIVER_STREAM] = net_init_stream, > + [NET_CLIENT_DRIVER_DGRAM] = net_init_dgram, > #ifdef CONFIG_VDE > [NET_CLIENT_DRIVER_VDE] = net_init_vde, > #endif > @@ -1101,6 +1104,8 @@ void show_netdevs(void) > int idx; > const char *available_netdevs[] = { > "socket", > + "stream", > + "dgram", > "hubport", > "tap", > #ifdef CONFIG_SLIRP > @@ -1612,7 +1617,19 @@ void net_init_clients(void) > */ > static bool netdev_is_modern(const char *optarg) > { > - return false; > + QDict *args; > + const char *type; > + bool is_modern; > + > + args = keyval_parse(optarg, "type", NULL, NULL); > + if (!args) { > + return false; > + } > + type = qdict_get_try_str(args, "type"); > + is_modern = !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); > + qobject_unref(args); > + > + return is_modern; > } You could use g_autoptr here: g_autoptr(QDict) args = NULL; const char *type; bool is_modern; args = keyval_parse(optarg, "type", NULL, NULL); if (!args) { return false; } type = qdict_get_try_str(args, "type"); return !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); Matter of taste; you decide. Now recall how this function is used: it decides whether to parse the modern way (with qobject_input_visitor_new_str()) or the traditional way (with qemu_opts_parse_noisily()). qemu_opts_parse_noisily() parses into a QemuOpts, for later use with the opts visitor. qobject_input_visitor_new_str() supports both dotted keys and JSON. The former is parsed with keyval_parse(), the latter with qobject_from_json(). It returns the resulting parse tree wrapped in a suitable QAPI input visitor. Issue 1: since we get there only when keyval_parse() succeeds, JSON is unreachable. Reproducer: $ qemu-system-x86_64 -netdev '{"id":"foo"}' upstream-qemu: -netdev {"id":"foo"}: Parameter 'id' is missing This is parsed with qemu_opts_parse_noisily(), resulting in a QemuOpts with a single option 'type' with value '{"id":"foo"}'. The error message comes from the opts visitor. To fix this, make netdev_is_modern() return true when optarg[0] == '{'. This matches how qobject_input_visitor_new_str() recognizes JSON. Issue 2: when keyval_parse() detects an error, we throw it away and fall back to QemuOpts. This is commonly what we want. But not always. For instance: $ qemu-system-x86_64 -netdev 'type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off' Note the typo "ipv4-off" instead of ipv4=off. The error reporting is crap: qemu-system-x86_64: -netdev type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off: warning: short-form boolean option 'addr.ipv4-off' deprecated Please use addr.ipv4-off=on instead qemu-system-x86_64: -netdev type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off: Parameter 'type' is missing We get this because netdev_is_modern() guesses wrongly: keyval_parse() fails with the perfectly reasonable error message "Expected '=' after parameter 'addr.ipv4-off'", but netdev_is_modern() ignores the error, and fails. We fall back to QemuOpts, and confusion ensues. I'm not sure we can do much better with reasonable effort. If we decide to accept this behavior, it should be documented at least in the source code. > > /* > diff --git a/net/stream.c b/net/stream.c > new file mode 100644 > index 000000000000..fdc97ee43a56 > --- /dev/null > +++ b/net/stream.c > @@ -0,0 +1,425 @@ > +/* > + * QEMU System Emulator > + * > + * Copyright (c) 2003-2008 Fabrice Bellard > + * > + * Permission is hereby granted, free of charge, to any person obtaining a copy > + * of this software and associated documentation files (the "Software"), to deal > + * in the Software without restriction, including without limitation the rights > + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell > + * copies of the Software, and to permit persons to whom the Software is > + * furnished to do so, subject to the following conditions: > + * > + * The above copyright notice and this permission notice shall be included in > + * all copies or substantial portions of the Software. > + * > + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR > + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, > + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL > + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER > + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, > + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN > + * THE SOFTWARE. > + */ Blank line here, please. > +#include "qemu/osdep.h" > + > +#include "net/net.h" > +#include "clients.h" > +#include "monitor/monitor.h" > +#include "qapi/error.h" > +#include "qemu/error-report.h" > +#include "qemu/option.h" > +#include "qemu/sockets.h" > +#include "qemu/iov.h" > +#include "qemu/main-loop.h" > +#include "qemu/cutils.h" > + > +typedef struct NetStreamState { > + NetClientState nc; > + int listen_fd; > + int fd; > + SocketReadState rs; > + unsigned int send_index; /* number of bytes sent*/ > + IOHandler *send_fn; > + bool read_poll; /* waiting to receive data? */ > + bool write_poll; /* waiting to transmit data? */ > +} NetStreamState; > + > +static void net_stream_accept(void *opaque); > +static void net_stream_writable(void *opaque); > + > +static void net_stream_update_fd_handler(NetStreamState *s) > +{ > + qemu_set_fd_handler(s->fd, > + s->read_poll ? s->send_fn : NULL, > + s->write_poll ? net_stream_writable : NULL, > + s); > +} > + > +static void net_stream_read_poll(NetStreamState *s, bool enable) > +{ > + s->read_poll = enable; > + net_stream_update_fd_handler(s); > +} > + > +static void net_stream_write_poll(NetStreamState *s, bool enable) > +{ > + s->write_poll = enable; > + net_stream_update_fd_handler(s); > +} > + > +static void net_stream_writable(void *opaque) > +{ > + NetStreamState *s = opaque; > + > + net_stream_write_poll(s, false); > + > + qemu_flush_queued_packets(&s->nc); > +} > + > +static ssize_t net_stream_receive(NetClientState *nc, const uint8_t *buf, > + size_t size) > +{ > + NetStreamState *s = DO_UPCAST(NetStreamState, nc, nc); > + uint32_t len = htonl(size); > + struct iovec iov[] = { > + { > + .iov_base = &len, > + .iov_len = sizeof(len), > + }, { > + .iov_base = (void *)buf, > + .iov_len = size, > + }, > + }; > + size_t remaining; > + ssize_t ret; > + > + remaining = iov_size(iov, 2) - s->send_index; > + ret = iov_send(s->fd, iov, 2, s->send_index, remaining); > + > + if (ret == -1 && errno == EAGAIN) { > + ret = 0; /* handled further down */ > + } > + if (ret == -1) { > + s->send_index = 0; > + return -errno; > + } > + if (ret < (ssize_t)remaining) { > + s->send_index += ret; > + net_stream_write_poll(s, true); > + return 0; > + } > + s->send_index = 0; > + return size; > +} > + > +static void net_stream_send_completed(NetClientState *nc, ssize_t len) > +{ > + NetStreamState *s = DO_UPCAST(NetStreamState, nc, nc); > + > + if (!s->read_poll) { > + net_stream_read_poll(s, true); > + } > +} > + > +static void net_stream_rs_finalize(SocketReadState *rs) > +{ > + NetStreamState *s = container_of(rs, NetStreamState, rs); > + > + if (qemu_send_packet_async(&s->nc, rs->buf, > + rs->packet_len, > + net_stream_send_completed) == 0) { > + net_stream_read_poll(s, false); > + } > +} > + > +static void net_stream_send(void *opaque) > +{ > + NetStreamState *s = opaque; > + int size; > + int ret; > + uint8_t buf1[NET_BUFSIZE]; > + const uint8_t *buf; > + > + size = recv(s->fd, buf1, sizeof(buf1), 0); > + if (size < 0) { > + if (errno != EWOULDBLOCK) { > + goto eoc; > + } > + } else if (size == 0) { > + /* end of connection */ > + eoc: > + net_stream_read_poll(s, false); > + net_stream_write_poll(s, false); > + if (s->listen_fd != -1) { > + qemu_set_fd_handler(s->listen_fd, net_stream_accept, NULL, s); > + } > + closesocket(s->fd); > + > + s->fd = -1; > + net_socket_rs_init(&s->rs, net_stream_rs_finalize, false); > + s->nc.link_down = true; > + memset(s->nc.info_str, 0, sizeof(s->nc.info_str)); > + > + return; > + } > + buf = buf1; > + > + ret = net_fill_rstate(&s->rs, buf, size); > + > + if (ret == -1) { > + goto eoc; > + } > +} > + > +static void net_stream_cleanup(NetClientState *nc) > +{ > + NetStreamState *s = DO_UPCAST(NetStreamState, nc, nc); > + if (s->fd != -1) { > + net_stream_read_poll(s, false); > + net_stream_write_poll(s, false); > + close(s->fd); > + s->fd = -1; > + } > + if (s->listen_fd != -1) { > + qemu_set_fd_handler(s->listen_fd, NULL, NULL, NULL); > + closesocket(s->listen_fd); > + s->listen_fd = -1; > + } > +} > + > +static void net_stream_connect(void *opaque) > +{ > + NetStreamState *s = opaque; > + s->send_fn = net_stream_send; > + net_stream_read_poll(s, true); > +} > + > +static NetClientInfo net_stream_info = { > + .type = NET_CLIENT_DRIVER_STREAM, > + .size = sizeof(NetStreamState), > + .receive = net_stream_receive, > + .cleanup = net_stream_cleanup, > +}; > + > +static NetStreamState *net_stream_fd_init_stream(NetClientState *peer, > + const char *model, > + const char *name, > + int fd, int is_connected) > +{ > + NetClientState *nc; > + NetStreamState *s; > + > + nc = qemu_new_net_client(&net_stream_info, peer, model, name); > + > + snprintf(nc->info_str, sizeof(nc->info_str), "fd=%d", fd); > + > + s = DO_UPCAST(NetStreamState, nc, nc); > + > + s->fd = fd; > + s->listen_fd = -1; > + net_socket_rs_init(&s->rs, net_stream_rs_finalize, false); > + > + /* Disable Nagle algorithm on TCP sockets to reduce latency */ > + socket_set_nodelay(fd); > + > + if (is_connected) { > + net_stream_connect(s); > + } else { > + qemu_set_fd_handler(s->fd, NULL, net_stream_connect, s); > + } > + return s; > +} > + > +static void net_stream_accept(void *opaque) > +{ > + NetStreamState *s = opaque; > + struct sockaddr_in saddr; > + socklen_t len; > + int fd; > + > + for (;;) { > + len = sizeof(saddr); > + fd = qemu_accept(s->listen_fd, (struct sockaddr *)&saddr, &len); > + if (fd < 0 && errno != EINTR) { > + return; > + } else if (fd >= 0) { > + qemu_set_fd_handler(s->listen_fd, NULL, NULL, NULL); > + break; > + } > + } > + > + s->fd = fd; > + s->nc.link_down = false; > + net_stream_connect(s); > + snprintf(s->nc.info_str, sizeof(s->nc.info_str), > + "connection from %s:%d", > + inet_ntoa(saddr.sin_addr), ntohs(saddr.sin_port)); > +} > + > +static int net_stream_server_init(NetClientState *peer, > + const char *model, > + const char *name, > + SocketAddress *addr, > + Error **errp) > +{ > + NetClientState *nc; > + NetStreamState *s; > + int fd, ret; > + > + switch (addr->type) { > + case SOCKET_ADDRESS_TYPE_INET: { > + struct sockaddr_in saddr_in; > + > + if (convert_host_port(&saddr_in, addr->u.inet.host, addr->u.inet.port, > + errp) < 0) { > + return -1; > + } > + > + fd = qemu_socket(PF_INET, SOCK_STREAM, 0); > + if (fd < 0) { > + error_setg_errno(errp, errno, "can't create stream socket"); > + return -1; > + } > + qemu_socket_set_nonblock(fd); > + > + socket_set_fast_reuse(fd); > + > + ret = bind(fd, (struct sockaddr *)&saddr_in, sizeof(saddr_in)); > + if (ret < 0) { > + error_setg_errno(errp, errno, "can't bind ip=%s to socket", > + inet_ntoa(saddr_in.sin_addr)); > + closesocket(fd); > + return -1; > + } > + break; > + } > + case SOCKET_ADDRESS_TYPE_UNIX: { > + error_setg(errp, "only support inet type"); > + return -1; > + } > + case SOCKET_ADDRESS_TYPE_FD: > + fd = monitor_fd_param(monitor_cur(), addr->u.fd.str, errp); > + if (fd == -1) { > + return -1; > + } > + ret = qemu_socket_try_set_nonblock(fd); > + if (ret < 0) { > + error_setg_errno(errp, -ret, "%s: Can't use file descriptor %d", > + name, fd); > + return -1; > + } > + break; > + default: > + g_assert_not_reached(); > + } > + > + ret = listen(fd, 0); > + if (ret < 0) { > + error_setg_errno(errp, errno, "can't listen on socket"); > + closesocket(fd); > + return -1; > + } > + > + nc = qemu_new_net_client(&net_stream_info, peer, model, name); > + s = DO_UPCAST(NetStreamState, nc, nc); > + s->fd = -1; > + s->listen_fd = fd; > + s->nc.link_down = true; > + net_socket_rs_init(&s->rs, net_stream_rs_finalize, false); > + > + qemu_set_fd_handler(s->listen_fd, net_stream_accept, NULL, s); > + return 0; > +} > + > +static int net_stream_client_init(NetClientState *peer, > + const char *model, > + const char *name, > + SocketAddress *addr, > + Error **errp) > +{ > + NetStreamState *s; > + int fd, connected, ret; > + gchar *info_str; > + > + switch (addr->type) { > + case SOCKET_ADDRESS_TYPE_INET: { > + struct sockaddr_in saddr_in; > + > + if (convert_host_port(&saddr_in, addr->u.inet.host, addr->u.inet.port, > + errp) < 0) { > + return -1; > + } > + > + fd = qemu_socket(PF_INET, SOCK_STREAM, 0); > + if (fd < 0) { > + error_setg_errno(errp, errno, "can't create stream socket"); > + return -1; > + } > + qemu_socket_set_nonblock(fd); > + > + connected = 0; > + for (;;) { > + ret = connect(fd, (struct sockaddr *)&saddr_in, sizeof(saddr_in)); > + if (ret < 0) { > + if (errno == EINTR || errno == EWOULDBLOCK) { > + /* continue */ > + } else if (errno == EINPROGRESS || > + errno == EALREADY || > + errno == EINVAL) { > + break; > + } else { > + error_setg_errno(errp, errno, "can't connect socket"); > + closesocket(fd); > + return -1; > + } > + } else { > + connected = 1; > + break; > + } > + } > + info_str = g_strdup_printf("connect to %s:%d", > + inet_ntoa(saddr_in.sin_addr), > + ntohs(saddr_in.sin_port)); > + break; > + } > + case SOCKET_ADDRESS_TYPE_FD: > + fd = monitor_fd_param(monitor_cur(), addr->u.fd.str, errp); > + if (fd == -1) { > + return -1; > + } > + ret = qemu_socket_try_set_nonblock(fd); > + if (ret < 0) { > + error_setg_errno(errp, -ret, "%s: Can't use file descriptor %d", > + name, fd); > + return -1; > + } > + connected = 1; > + info_str = g_strdup_printf("connect to fd %d", fd); > + break; > + default: > + error_setg(errp, "only support inet or fd type"); > + return -1; > + } > + > + s = net_stream_fd_init_stream(peer, model, name, fd, connected); > + > + pstrcpy(s->nc.info_str, sizeof(s->nc.info_str), info_str); > + g_free(info_str); > + > + return 0; > +} > + > +int net_init_stream(const Netdev *netdev, const char *name, > + NetClientState *peer, Error **errp) > +{ > + const NetdevStreamOptions *sock; > + > + assert(netdev->type == NET_CLIENT_DRIVER_STREAM); > + sock = &netdev->u.stream; > + > + if (!sock->has_server || sock->server) { > + return net_stream_server_init(peer, "stream", name, sock->addr, errp); > + } > + return net_stream_client_init(peer, "stream", name, sock->addr, errp); > +} > diff --git a/qapi/net.json b/qapi/net.json > index d6f7cfd4d656..5f72995b1d24 100644 > --- a/qapi/net.json > +++ b/qapi/net.json > @@ -7,6 +7,7 @@ > ## > > { 'include': 'common.json' } > +{ 'include': 'sockets.json' } > > ## > # @set_link: > @@ -566,6 +567,37 @@ > '*isolated': 'bool' }, > 'if': 'CONFIG_VMNET' } > > +## > +# @NetdevStreamOptions: > +# > +# Configuration info for stream socket netdev > +# > +# @addr: socket address to listen on (server=true) > +# or connect to (server=false) > +# @server: create server socket (default: true) > +# > +# Since: 7.1 > +## > +{ 'struct': 'NetdevStreamOptions', > + 'data': { > + 'addr': 'SocketAddress', > + '*server': 'bool' } } > + > +## > +# @NetdevDgramOptions: > +# > +# Configuration info for datagram socket netdev. > +# > +# @remote: remote address > +# @local: local address > +# > +# Since: 7.1 > +## > +{ 'struct': 'NetdevDgramOptions', > + 'data': { > + '*local': 'SocketAddress', > + '*remote': 'SocketAddress' } } In review of v2, I inquired about behavior when members are absent. You wrote: The code checks there is at least one of these options and reports an error if not. if remote address is present and it's a multicast address, local address is optional. otherwise local address is required and remote address is optional. Please work that into the doc comment. > + > ## > # @NetClientDriver: > # > @@ -579,9 +611,9 @@ > # @vmnet-bridged since 7.1 > ## > { 'enum': 'NetClientDriver', > - 'data': [ 'none', 'nic', 'user', 'tap', 'l2tpv3', 'socket', 'vde', > - 'bridge', 'hubport', 'netmap', 'vhost-user', 'vhost-vdpa', > - { 'name': 'vmnet-host', 'if': 'CONFIG_VMNET' }, > + 'data': [ 'none', 'nic', 'user', 'tap', 'l2tpv3', 'socket', 'stream', > + 'dgram', 'vde', 'bridge', 'hubport', 'netmap', 'vhost-user', > + 'vhost-vdpa', { 'name': 'vmnet-host', 'if': 'CONFIG_VMNET' }, > { 'name': 'vmnet-shared', 'if': 'CONFIG_VMNET' }, > { 'name': 'vmnet-bridged', 'if': 'CONFIG_VMNET' }] } Opportunity to put vmnet-host on its own line for readability. > > @@ -610,6 +642,8 @@ > 'tap': 'NetdevTapOptions', > 'l2tpv3': 'NetdevL2TPv3Options', > 'socket': 'NetdevSocketOptions', > + 'stream': 'NetdevStreamOptions', > + 'dgram': 'NetdevDgramOptions', > 'vde': 'NetdevVdeOptions', > 'bridge': 'NetdevBridgeOptions', > 'hubport': 'NetdevHubPortOptions', > diff --git a/qemu-options.hx b/qemu-options.hx > index 60cf188da429..c87dcdc65ece 100644 > --- a/qemu-options.hx > +++ b/qemu-options.hx > @@ -2727,6 +2727,18 @@ DEF("netdev", HAS_ARG, QEMU_OPTION_netdev, > "-netdev socket,id=str[,fd=h][,udp=host:port][,localaddr=host:port]\n" > " configure a network backend to connect to another network\n" > " using an UDP tunnel\n" > + "-netdev stream,id=str[,server=on|off],addr.type=inet,addr.host=host,addr.port=port\n" > + "-netdev stream,id=str[,server=on|off],addr.type=fd,addr.str=h\n" > + " configure a network backend to connect to another network\n" > + " using a socket connection in stream mode.\n" > + "-netdev dgram,id=str,remote.type=inet,remote.host=maddr,remote.port=port[,local.type=inet,local.host=addr]\n" > + "-netdev dgram,id=str,remote.type=inet,remote.host=maddr,remote.port=port[,local.type=fd,local.str=h]\n" > + " configure a network backend to connect to a multicast maddr and port\n" > + " use 'local.host=addr' to specify the host address to send packets from\n" > + "-netdev dgram,id=str,local.type=inet,local.host=host,local.port=port[,remote.type=inet,remote.host=host,remote.port=port]\n" > + "-netdev dgram,id=str,local.type=fd,local.str=h\n" > + " configure a network backend to connect to another network\n" > + " using an UDP tunnel\n" > #ifdef CONFIG_VDE > "-netdev vde,id=str[,sock=socketpath][,port=n][,group=groupname][,mode=octalmode]\n" > " configure a network backend to connect to port 'n' of a vde switch\n"
On 20/06/2022 17:21, Markus Armbruster wrote: > Laurent Vivier <lvivier@redhat.com> writes: > >> Copied from socket netdev file and modified to use SocketAddress >> to be able to introduce new features like unix socket. >> >> "udp" and "mcast" are squashed into dgram netdev, multicast is detected >> according to the IP address type. >> "listen" and "connect" modes are managed by stream netdev. An optional >> parameter "server" defines the mode (server by default) >> >> Signed-off-by: Laurent Vivier <lvivier@redhat.com> >> Reviewed-by: Stefano Brivio <sbrivio@redhat.com> >> --- >> hmp-commands.hx | 2 +- >> net/clients.h | 6 + >> net/dgram.c | 630 ++++++++++++++++++++++++++++++++++++++++++++++++ >> net/hub.c | 2 + >> net/meson.build | 2 + >> net/net.c | 19 +- >> net/stream.c | 425 ++++++++++++++++++++++++++++++++ >> qapi/net.json | 40 ++- >> qemu-options.hx | 12 + >> 9 files changed, 1133 insertions(+), 5 deletions(-) >> create mode 100644 net/dgram.c >> create mode 100644 net/stream.c >> ... >> diff --git a/net/dgram.c b/net/dgram.c >> new file mode 100644 >> index 000000000000..aa4240501ed0 >> --- /dev/null >> +++ b/net/dgram.c >> @@ -0,0 +1,630 @@ >> +/* >> + * QEMU System Emulator >> + * >> + * Copyright (c) 2003-2008 Fabrice Bellard >> + * >> + * Permission is hereby granted, free of charge, to any person obtaining a copy >> + * of this software and associated documentation files (the "Software"), to deal >> + * in the Software without restriction, including without limitation the rights >> + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell >> + * copies of the Software, and to permit persons to whom the Software is >> + * furnished to do so, subject to the following conditions: >> + * >> + * The above copyright notice and this permission notice shall be included in >> + * all copies or substantial portions of the Software. >> + * >> + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR >> + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, >> + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL >> + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER >> + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, >> + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN >> + * THE SOFTWARE. >> + */ > > Blank line here, please. Done ... >> diff --git a/net/net.c b/net/net.c >> index c337d3d753fe..440957b272ee 100644 >> --- a/net/net.c >> +++ b/net/net.c ... >> @@ -1612,7 +1617,19 @@ void net_init_clients(void) >> */ >> static bool netdev_is_modern(const char *optarg) >> { >> - return false; >> + QDict *args; >> + const char *type; >> + bool is_modern; >> + >> + args = keyval_parse(optarg, "type", NULL, NULL); >> + if (!args) { >> + return false; >> + } >> + type = qdict_get_try_str(args, "type"); >> + is_modern = !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); >> + qobject_unref(args); >> + >> + return is_modern; >> } > > You could use g_autoptr here: > > g_autoptr(QDict) args = NULL; > const char *type; > bool is_modern; > > args = keyval_parse(optarg, "type", NULL, NULL); > if (!args) { > return false; > } > type = qdict_get_try_str(args, "type"); > return !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); > > Matter of taste; you decide. Looks good. We already had some series to convert existing code to g_autoptr(), so it seems the way to do. > > Now recall how this function is used: it decides whether to parse the > modern way (with qobject_input_visitor_new_str()) or the traditional way > (with qemu_opts_parse_noisily()). > > qemu_opts_parse_noisily() parses into a QemuOpts, for later use with the > opts visitor. > > qobject_input_visitor_new_str() supports both dotted keys and JSON. The > former is parsed with keyval_parse(), the latter with > qobject_from_json(). It returns the resulting parse tree wrapped in a > suitable QAPI input visitor. > > Issue 1: since we get there only when keyval_parse() succeeds, JSON is > unreachable. Reproducer: > > $ qemu-system-x86_64 -netdev '{"id":"foo"}' > upstream-qemu: -netdev {"id":"foo"}: Parameter 'id' is missing > > This is parsed with qemu_opts_parse_noisily(), resulting in a QemuOpts > with a single option 'type' with value '{"id":"foo"}'. The error > message comes from the opts visitor. > > To fix this, make netdev_is_modern() return true when optarg[0] == '{'. > This matches how qobject_input_visitor_new_str() recognizes JSON. OK > > Issue 2: when keyval_parse() detects an error, we throw it away and fall > back to QemuOpts. This is commonly what we want. But not always. For > instance: > > $ qemu-system-x86_64 -netdev 'type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off' > > Note the typo "ipv4-off" instead of ipv4=off. The error reporting is crap: > > qemu-system-x86_64: -netdev type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off: warning: short-form boolean option 'addr.ipv4-off' deprecated > Please use addr.ipv4-off=on instead > qemu-system-x86_64: -netdev type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off: Parameter 'type' is missing > > We get this because netdev_is_modern() guesses wrongly: keyval_parse() > fails with the perfectly reasonable error message "Expected '=' after > parameter 'addr.ipv4-off'", but netdev_is_modern() ignores the error, > and fails. We fall back to QemuOpts, and confusion ensues. > > I'm not sure we can do much better with reasonable effort. If we decide > to accept this behavior, it should be documented at least in the source > code. What about using modern syntax by default? args = keyval_parse(optarg, "type", NULL, NULL); if (!args) { /* cannot detect the syntax, use new style syntax */ return true; } >> >> /* >> diff --git a/net/stream.c b/net/stream.c >> new file mode 100644 >> index 000000000000..fdc97ee43a56 >> --- /dev/null >> +++ b/net/stream.c >> @@ -0,0 +1,425 @@ >> +/* >> + * QEMU System Emulator >> + * >> + * Copyright (c) 2003-2008 Fabrice Bellard >> + * >> + * Permission is hereby granted, free of charge, to any person obtaining a copy >> + * of this software and associated documentation files (the "Software"), to deal >> + * in the Software without restriction, including without limitation the rights >> + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell >> + * copies of the Software, and to permit persons to whom the Software is >> + * furnished to do so, subject to the following conditions: >> + * >> + * The above copyright notice and this permission notice shall be included in >> + * all copies or substantial portions of the Software. >> + * >> + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR >> + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, >> + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL >> + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER >> + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, >> + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN >> + * THE SOFTWARE. >> + */ > > Blank line here, please. Done ... >> diff --git a/qapi/net.json b/qapi/net.json >> index d6f7cfd4d656..5f72995b1d24 100644 >> --- a/qapi/net.json >> +++ b/qapi/net.json ... >> @@ -566,6 +567,37 @@ >> '*isolated': 'bool' }, >> 'if': 'CONFIG_VMNET' } >> >> +## >> +# @NetdevStreamOptions: >> +# >> +# Configuration info for stream socket netdev >> +# >> +# @addr: socket address to listen on (server=true) >> +# or connect to (server=false) >> +# @server: create server socket (default: true) >> +# >> +# Since: 7.1 >> +## >> +{ 'struct': 'NetdevStreamOptions', >> + 'data': { >> + 'addr': 'SocketAddress', >> + '*server': 'bool' } } >> + >> +## >> +# @NetdevDgramOptions: >> +# >> +# Configuration info for datagram socket netdev. >> +# >> +# @remote: remote address >> +# @local: local address >> +# >> +# Since: 7.1 >> +## >> +{ 'struct': 'NetdevDgramOptions', >> + 'data': { >> + '*local': 'SocketAddress', >> + '*remote': 'SocketAddress' } } > > In review of v2, I inquired about behavior when members are absent. > You wrote: > > The code checks there is at least one of these options and reports an error if not. > > if remote address is present and it's a multicast address, local address is optional. > > otherwise local address is required and remote address is optional. > > Please work that into the doc comment. OK > >> + >> ## >> # @NetClientDriver: >> # >> @@ -579,9 +611,9 @@ >> # @vmnet-bridged since 7.1 >> ## >> { 'enum': 'NetClientDriver', >> - 'data': [ 'none', 'nic', 'user', 'tap', 'l2tpv3', 'socket', 'vde', >> - 'bridge', 'hubport', 'netmap', 'vhost-user', 'vhost-vdpa', >> - { 'name': 'vmnet-host', 'if': 'CONFIG_VMNET' }, >> + 'data': [ 'none', 'nic', 'user', 'tap', 'l2tpv3', 'socket', 'stream', >> + 'dgram', 'vde', 'bridge', 'hubport', 'netmap', 'vhost-user', >> + 'vhost-vdpa', { 'name': 'vmnet-host', 'if': 'CONFIG_VMNET' }, >> { 'name': 'vmnet-shared', 'if': 'CONFIG_VMNET' }, >> { 'name': 'vmnet-bridged', 'if': 'CONFIG_VMNET' }] } > > Opportunity to put vmnet-host on its own line for readability. > OK Thanks, Laurent
Laurent Vivier <lvivier@redhat.com> writes: > On 20/06/2022 17:21, Markus Armbruster wrote: >> Laurent Vivier <lvivier@redhat.com> writes: >> >>> Copied from socket netdev file and modified to use SocketAddress >>> to be able to introduce new features like unix socket. >>> >>> "udp" and "mcast" are squashed into dgram netdev, multicast is detected >>> according to the IP address type. >>> "listen" and "connect" modes are managed by stream netdev. An optional >>> parameter "server" defines the mode (server by default) >>> >>> Signed-off-by: Laurent Vivier <lvivier@redhat.com> >>> Reviewed-by: Stefano Brivio <sbrivio@redhat.com> >>> --- [...] >>> diff --git a/net/net.c b/net/net.c >>> index c337d3d753fe..440957b272ee 100644 >>> --- a/net/net.c >>> +++ b/net/net.c > ... >>> @@ -1612,7 +1617,19 @@ void net_init_clients(void) >>> */ >>> static bool netdev_is_modern(const char *optarg) >>> { >>> - return false; >>> + QDict *args; >>> + const char *type; >>> + bool is_modern; >>> + >>> + args = keyval_parse(optarg, "type", NULL, NULL); >>> + if (!args) { >>> + return false; >>> + } >>> + type = qdict_get_try_str(args, "type"); >>> + is_modern = !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); >>> + qobject_unref(args); >>> + >>> + return is_modern; >>> } >> >> You could use g_autoptr here: >> >> g_autoptr(QDict) args = NULL; >> const char *type; >> bool is_modern; >> >> args = keyval_parse(optarg, "type", NULL, NULL); >> if (!args) { >> return false; >> } >> type = qdict_get_try_str(args, "type"); >> return !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); >> >> Matter of taste; you decide. > > Looks good. We already had some series to convert existing code to g_autoptr(), so it > seems the way to do. > >> >> Now recall how this function is used: it decides whether to parse the >> modern way (with qobject_input_visitor_new_str()) or the traditional way >> (with qemu_opts_parse_noisily()). >> >> qemu_opts_parse_noisily() parses into a QemuOpts, for later use with the >> opts visitor. >> >> qobject_input_visitor_new_str() supports both dotted keys and JSON. The >> former is parsed with keyval_parse(), the latter with >> qobject_from_json(). It returns the resulting parse tree wrapped in a >> suitable QAPI input visitor. >> >> Issue 1: since we get there only when keyval_parse() succeeds, JSON is >> unreachable. Reproducer: >> >> $ qemu-system-x86_64 -netdev '{"id":"foo"}' >> upstream-qemu: -netdev {"id":"foo"}: Parameter 'id' is missing >> >> This is parsed with qemu_opts_parse_noisily(), resulting in a QemuOpts >> with a single option 'type' with value '{"id":"foo"}'. The error >> message comes from the opts visitor. >> >> To fix this, make netdev_is_modern() return true when optarg[0] == '{'. >> This matches how qobject_input_visitor_new_str() recognizes JSON. > > OK > >> >> Issue 2: when keyval_parse() detects an error, we throw it away and fall >> back to QemuOpts. This is commonly what we want. But not always. For >> instance: >> >> $ qemu-system-x86_64 -netdev 'type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off' >> >> Note the typo "ipv4-off" instead of ipv4=off. The error reporting is crap: >> >> qemu-system-x86_64: -netdev type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off: warning: short-form boolean option 'addr.ipv4-off' deprecated >> Please use addr.ipv4-off=on instead >> qemu-system-x86_64: -netdev type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off: Parameter 'type' is missing >> >> We get this because netdev_is_modern() guesses wrongly: keyval_parse() >> fails with the perfectly reasonable error message "Expected '=' after >> parameter 'addr.ipv4-off'", but netdev_is_modern() ignores the error, >> and fails. We fall back to QemuOpts, and confusion ensues. >> >> I'm not sure we can do much better with reasonable effort. If we decide >> to accept this behavior, it should be documented at least in the source >> code. > > What about using modern syntax by default? > > args = keyval_parse(optarg, "type", NULL, NULL); > if (!args) { > /* cannot detect the syntax, use new style syntax */ > return true; > } As is, netdev_is_modern() has three cases: 1. keyval_parse() fails 2. keyval_parse() succeeds, but value of @type is not modern 3. keyval_parse() succeeds, and value of @type is modern In case 3. we're sure, because even if qemu_opts_parse_noisily() also succeeded, it would result in the same value of @type. In case 2, assuming traditional seems reasonable. The assumption can be wrong when the user intends modern, but fat-fingers the type=T part. In case 1, we know nothing. Guessing modern is wrong when the user intends traditional. This happens when a meant-to-be-traditional @optarg also parses as modern. Quite possible. Guessing traditional is wrong when the user intends modern. This happens when a meant-to-be-modern @optarg fails to parse as modern, i.e. whenever the user screws up modern syntax. Which guess is less bad? I'm not sure. Thoughts? Note that additionally checking whether qemu_opts_parse() succeeds would be next to useless, since qemu_opts_parse() accepts pretty much anything. [...]
On 21/06/2022 10:49, Markus Armbruster wrote: > Laurent Vivier <lvivier@redhat.com> writes: > >> On 20/06/2022 17:21, Markus Armbruster wrote: >>> Laurent Vivier <lvivier@redhat.com> writes: >>> >>>> Copied from socket netdev file and modified to use SocketAddress >>>> to be able to introduce new features like unix socket. >>>> >>>> "udp" and "mcast" are squashed into dgram netdev, multicast is detected >>>> according to the IP address type. >>>> "listen" and "connect" modes are managed by stream netdev. An optional >>>> parameter "server" defines the mode (server by default) >>>> >>>> Signed-off-by: Laurent Vivier <lvivier@redhat.com> >>>> Reviewed-by: Stefano Brivio <sbrivio@redhat.com> >>>> --- > > [...] > >>>> diff --git a/net/net.c b/net/net.c >>>> index c337d3d753fe..440957b272ee 100644 >>>> --- a/net/net.c >>>> +++ b/net/net.c >> ... >>>> @@ -1612,7 +1617,19 @@ void net_init_clients(void) >>>> */ >>>> static bool netdev_is_modern(const char *optarg) >>>> { >>>> - return false; >>>> + QDict *args; >>>> + const char *type; >>>> + bool is_modern; >>>> + >>>> + args = keyval_parse(optarg, "type", NULL, NULL); >>>> + if (!args) { >>>> + return false; >>>> + } >>>> + type = qdict_get_try_str(args, "type"); >>>> + is_modern = !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); >>>> + qobject_unref(args); >>>> + >>>> + return is_modern; >>>> } >>> >>> You could use g_autoptr here: >>> >>> g_autoptr(QDict) args = NULL; >>> const char *type; >>> bool is_modern; >>> >>> args = keyval_parse(optarg, "type", NULL, NULL); >>> if (!args) { >>> return false; >>> } >>> type = qdict_get_try_str(args, "type"); >>> return !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); >>> >>> Matter of taste; you decide. >> >> Looks good. We already had some series to convert existing code to g_autoptr(), so it >> seems the way to do. >> >>> >>> Now recall how this function is used: it decides whether to parse the >>> modern way (with qobject_input_visitor_new_str()) or the traditional way >>> (with qemu_opts_parse_noisily()). >>> >>> qemu_opts_parse_noisily() parses into a QemuOpts, for later use with the >>> opts visitor. >>> >>> qobject_input_visitor_new_str() supports both dotted keys and JSON. The >>> former is parsed with keyval_parse(), the latter with >>> qobject_from_json(). It returns the resulting parse tree wrapped in a >>> suitable QAPI input visitor. >>> >>> Issue 1: since we get there only when keyval_parse() succeeds, JSON is >>> unreachable. Reproducer: >>> >>> $ qemu-system-x86_64 -netdev '{"id":"foo"}' >>> upstream-qemu: -netdev {"id":"foo"}: Parameter 'id' is missing >>> >>> This is parsed with qemu_opts_parse_noisily(), resulting in a QemuOpts >>> with a single option 'type' with value '{"id":"foo"}'. The error >>> message comes from the opts visitor. >>> >>> To fix this, make netdev_is_modern() return true when optarg[0] == '{'. >>> This matches how qobject_input_visitor_new_str() recognizes JSON. >> >> OK >> >>> >>> Issue 2: when keyval_parse() detects an error, we throw it away and fall >>> back to QemuOpts. This is commonly what we want. But not always. For >>> instance: >>> >>> $ qemu-system-x86_64 -netdev 'type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off' >>> >>> Note the typo "ipv4-off" instead of ipv4=off. The error reporting is crap: >>> >>> qemu-system-x86_64: -netdev type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off: warning: short-form boolean option 'addr.ipv4-off' deprecated >>> Please use addr.ipv4-off=on instead >>> qemu-system-x86_64: -netdev type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off: Parameter 'type' is missing >>> >>> We get this because netdev_is_modern() guesses wrongly: keyval_parse() >>> fails with the perfectly reasonable error message "Expected '=' after >>> parameter 'addr.ipv4-off'", but netdev_is_modern() ignores the error, >>> and fails. We fall back to QemuOpts, and confusion ensues. >>> >>> I'm not sure we can do much better with reasonable effort. If we decide >>> to accept this behavior, it should be documented at least in the source >>> code. >> >> What about using modern syntax by default? >> >> args = keyval_parse(optarg, "type", NULL, NULL); >> if (!args) { >> /* cannot detect the syntax, use new style syntax */ >> return true; >> } > > As is, netdev_is_modern() has three cases: > > 1. keyval_parse() fails > > 2. keyval_parse() succeeds, but value of @type is not modern > > 3. keyval_parse() succeeds, and value of @type is modern > > In case 3. we're sure, because even if qemu_opts_parse_noisily() also > succeeded, it would result in the same value of @type. > > In case 2, assuming traditional seems reasonable. The assumption can be > wrong when the user intends modern, but fat-fingers the type=T part. > > In case 1, we know nothing. > > Guessing modern is wrong when the user intends traditional. This > happens when a meant-to-be-traditional @optarg also parses as modern. > Quite possible. I don't see why keyval_parse() fails in this case. Any example? > Guessing traditional is wrong when the user intends modern. This > happens when a meant-to-be-modern @optarg fails to parse as modern, > i.e. whenever the user screws up modern syntax. This one is the example you gave (ipv4-off) > Which guess is less bad? I'm not sure. Thoughts? Perhaps we can simply fail if keyval_parse() fails? something like: args = keyval_parse(optarg, "type", NULL, &error_fatal); type = qdict_get_try_str(args, "type"); return !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); Thanks, Laurent
Laurent Vivier <lvivier@redhat.com> writes: > On 21/06/2022 10:49, Markus Armbruster wrote: >> Laurent Vivier <lvivier@redhat.com> writes: >> >>> On 20/06/2022 17:21, Markus Armbruster wrote: >>>> Laurent Vivier <lvivier@redhat.com> writes: >>>> >>>>> Copied from socket netdev file and modified to use SocketAddress >>>>> to be able to introduce new features like unix socket. >>>>> >>>>> "udp" and "mcast" are squashed into dgram netdev, multicast is detected >>>>> according to the IP address type. >>>>> "listen" and "connect" modes are managed by stream netdev. An optional >>>>> parameter "server" defines the mode (server by default) >>>>> >>>>> Signed-off-by: Laurent Vivier <lvivier@redhat.com> >>>>> Reviewed-by: Stefano Brivio <sbrivio@redhat.com> >>>>> --- >> >> [...] >> >>>>> diff --git a/net/net.c b/net/net.c >>>>> index c337d3d753fe..440957b272ee 100644 >>>>> --- a/net/net.c >>>>> +++ b/net/net.c >>> ... >>>>> @@ -1612,7 +1617,19 @@ void net_init_clients(void) >>>>> */ >>>>> static bool netdev_is_modern(const char *optarg) >>>>> { >>>>> - return false; >>>>> + QDict *args; >>>>> + const char *type; >>>>> + bool is_modern; >>>>> + >>>>> + args = keyval_parse(optarg, "type", NULL, NULL); >>>>> + if (!args) { >>>>> + return false; >>>>> + } >>>>> + type = qdict_get_try_str(args, "type"); >>>>> + is_modern = !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); >>>>> + qobject_unref(args); >>>>> + >>>>> + return is_modern; >>>>> } >>>> >>>> You could use g_autoptr here: >>>> >>>> g_autoptr(QDict) args = NULL; >>>> const char *type; >>>> bool is_modern; >>>> >>>> args = keyval_parse(optarg, "type", NULL, NULL); >>>> if (!args) { >>>> return false; >>>> } >>>> type = qdict_get_try_str(args, "type"); >>>> return !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); >>>> >>>> Matter of taste; you decide. >>> >>> Looks good. We already had some series to convert existing code to g_autoptr(), so it >>> seems the way to do. >>> >>>> >>>> Now recall how this function is used: it decides whether to parse the >>>> modern way (with qobject_input_visitor_new_str()) or the traditional way >>>> (with qemu_opts_parse_noisily()). >>>> >>>> qemu_opts_parse_noisily() parses into a QemuOpts, for later use with the >>>> opts visitor. >>>> >>>> qobject_input_visitor_new_str() supports both dotted keys and JSON. The >>>> former is parsed with keyval_parse(), the latter with >>>> qobject_from_json(). It returns the resulting parse tree wrapped in a >>>> suitable QAPI input visitor. >>>> >>>> Issue 1: since we get there only when keyval_parse() succeeds, JSON is >>>> unreachable. Reproducer: >>>> >>>> $ qemu-system-x86_64 -netdev '{"id":"foo"}' >>>> upstream-qemu: -netdev {"id":"foo"}: Parameter 'id' is missing >>>> >>>> This is parsed with qemu_opts_parse_noisily(), resulting in a QemuOpts >>>> with a single option 'type' with value '{"id":"foo"}'. The error >>>> message comes from the opts visitor. >>>> >>>> To fix this, make netdev_is_modern() return true when optarg[0] == '{'. >>>> This matches how qobject_input_visitor_new_str() recognizes JSON. >>> >>> OK >>> >>>> >>>> Issue 2: when keyval_parse() detects an error, we throw it away and fall >>>> back to QemuOpts. This is commonly what we want. But not always. For >>>> instance: >>>> >>>> $ qemu-system-x86_64 -netdev 'type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off' >>>> >>>> Note the typo "ipv4-off" instead of ipv4=off. The error reporting is crap: >>>> >>>> qemu-system-x86_64: -netdev type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off: warning: short-form boolean option 'addr.ipv4-off' deprecated >>>> Please use addr.ipv4-off=on instead >>>> qemu-system-x86_64: -netdev type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off: Parameter 'type' is missing >>>> >>>> We get this because netdev_is_modern() guesses wrongly: keyval_parse() >>>> fails with the perfectly reasonable error message "Expected '=' after >>>> parameter 'addr.ipv4-off'", but netdev_is_modern() ignores the error, >>>> and fails. We fall back to QemuOpts, and confusion ensues. >>>> >>>> I'm not sure we can do much better with reasonable effort. If we decide >>>> to accept this behavior, it should be documented at least in the source >>>> code. >>> >>> What about using modern syntax by default? >>> >>> args = keyval_parse(optarg, "type", NULL, NULL); >>> if (!args) { >>> /* cannot detect the syntax, use new style syntax */ >>> return true; >>> } >> >> As is, netdev_is_modern() has three cases: >> >> 1. keyval_parse() fails >> >> 2. keyval_parse() succeeds, but value of @type is not modern >> >> 3. keyval_parse() succeeds, and value of @type is modern >> >> In case 3. we're sure, because even if qemu_opts_parse_noisily() also >> succeeded, it would result in the same value of @type. >> >> In case 2, assuming traditional seems reasonable. The assumption can be >> wrong when the user intends modern, but fat-fingers the type=T part. >> >> In case 1, we know nothing. >> >> Guessing modern is wrong when the user intends traditional. This >> happens when a meant-to-be-traditional @optarg also parses as modern. >> Quite possible. > > I don't see why keyval_parse() fails in this case. Any example? Brain cramp on my part, I'm afraid %-} Let me start over. Guessing modern is wrong when the user intends traditional. Two sub-cases then: * @optarg parses fine as traditional. For instance, $ qemu-system-x86_64 -netdev type=user,id=foo,ipv4 parses with a warning: option 'ipv4' deprecated Please use ipv4=on instead This is how current master behaves. Guessing modern makes this fail instead: qemu-system-x86_64: -netdev type=user,id=foo,ipv4: Expected '=' after parameter 'ipv4' Regression. * @optarg fails to parse traditional, too. The error reporting is for modern even though the user intends traditional. Can be misleading. Example: $ qemu-system-x86_64 -netdev type=user,id=_,ipv4 Current master: qemu-system-x86_64: -netdev type=user,id=_,ipv4: Parameter 'id' expects an identifier Identifiers consist of letters, digits, '-', '.', '_', starting with a letter. Guessing modern instead: qemu-system-x86_64: -netdev type=user,id=_,ipv4: Expected '=' after parameter 'ipv4' This should be rare in practice, as traditional parsing detects very few errors. >> Guessing traditional is wrong when the user intends modern. This >> happens when a meant-to-be-modern @optarg fails to parse as modern, >> i.e. whenever the user screws up modern syntax. > > This one is the example you gave (ipv4-off) Two sub-cases then: * @optarg parses fine as traditional. The parse result is unlikely to make sense, though. For instance, $ qemu-system-x86_64 -netdev type=stream,id=foo,server parses with a warning: qemu-system-x86_64: -netdev type=stream,id=foo,server: warning: short-form boolean option 'server' deprecated Please use server=on instead But the result fails in the opts visitor: qemu-system-x86_64: -netdev type=stream,id=foo,server: Parameter 'type' is missing In this case, we're better off with guessing modern: qemu-system-x86_64: -netdev type=stream,id=foo,server: Expected '=' after parameter * @optarg fails to parse traditional, too. The error reporting is for traditional even though the user intends modern. Can be misleading. This is my ipv4-off example. Can't win. Parsers simply don't compose that way. >> Which guess is less bad? I'm not sure. Thoughts? > > Perhaps we can simply fail if keyval_parse() fails? > > something like: > > args = keyval_parse(optarg, "type", NULL, &error_fatal); > type = qdict_get_try_str(args, "type"); > return !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); This rejects working option arguments that don't also parse as modern, such as "-netdev type=user,id=foo,ipv4". Guessing traditional seems to be the least bad solution so far. Supporting both traditional and modern syntax in an option argument is a swamp. Can we bypass it somehow? -object uses traditional QemuOpts parsing for key=value,..., and modern parsing for JSON. Sticking to traditional sidesteps compatibility issues. But you have to use JSON for things traditional can't express. Thoughts?
On 22/06/2022 13:47, Markus Armbruster wrote: > Laurent Vivier <lvivier@redhat.com> writes: > >> On 21/06/2022 10:49, Markus Armbruster wrote: >>> Laurent Vivier <lvivier@redhat.com> writes: >>> >>>> On 20/06/2022 17:21, Markus Armbruster wrote: >>>>> Laurent Vivier <lvivier@redhat.com> writes: >>>>> >>>>>> Copied from socket netdev file and modified to use SocketAddress >>>>>> to be able to introduce new features like unix socket. >>>>>> >>>>>> "udp" and "mcast" are squashed into dgram netdev, multicast is detected >>>>>> according to the IP address type. >>>>>> "listen" and "connect" modes are managed by stream netdev. An optional >>>>>> parameter "server" defines the mode (server by default) >>>>>> >>>>>> Signed-off-by: Laurent Vivier <lvivier@redhat.com> >>>>>> Reviewed-by: Stefano Brivio <sbrivio@redhat.com> >>>>>> --- >>> >>> [...] >>> >>>>>> diff --git a/net/net.c b/net/net.c >>>>>> index c337d3d753fe..440957b272ee 100644 >>>>>> --- a/net/net.c >>>>>> +++ b/net/net.c >>>> ... >>>>>> @@ -1612,7 +1617,19 @@ void net_init_clients(void) >>>>>> */ >>>>>> static bool netdev_is_modern(const char *optarg) >>>>>> { >>>>>> - return false; >>>>>> + QDict *args; >>>>>> + const char *type; >>>>>> + bool is_modern; >>>>>> + >>>>>> + args = keyval_parse(optarg, "type", NULL, NULL); >>>>>> + if (!args) { >>>>>> + return false; >>>>>> + } >>>>>> + type = qdict_get_try_str(args, "type"); >>>>>> + is_modern = !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); >>>>>> + qobject_unref(args); >>>>>> + >>>>>> + return is_modern; >>>>>> } >>>>> >>>>> You could use g_autoptr here: >>>>> >>>>> g_autoptr(QDict) args = NULL; >>>>> const char *type; >>>>> bool is_modern; >>>>> >>>>> args = keyval_parse(optarg, "type", NULL, NULL); >>>>> if (!args) { >>>>> return false; >>>>> } >>>>> type = qdict_get_try_str(args, "type"); >>>>> return !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); >>>>> >>>>> Matter of taste; you decide. >>>> >>>> Looks good. We already had some series to convert existing code to g_autoptr(), so it >>>> seems the way to do. >>>> >>>>> >>>>> Now recall how this function is used: it decides whether to parse the >>>>> modern way (with qobject_input_visitor_new_str()) or the traditional way >>>>> (with qemu_opts_parse_noisily()). >>>>> >>>>> qemu_opts_parse_noisily() parses into a QemuOpts, for later use with the >>>>> opts visitor. >>>>> >>>>> qobject_input_visitor_new_str() supports both dotted keys and JSON. The >>>>> former is parsed with keyval_parse(), the latter with >>>>> qobject_from_json(). It returns the resulting parse tree wrapped in a >>>>> suitable QAPI input visitor. >>>>> >>>>> Issue 1: since we get there only when keyval_parse() succeeds, JSON is >>>>> unreachable. Reproducer: >>>>> >>>>> $ qemu-system-x86_64 -netdev '{"id":"foo"}' >>>>> upstream-qemu: -netdev {"id":"foo"}: Parameter 'id' is missing >>>>> >>>>> This is parsed with qemu_opts_parse_noisily(), resulting in a QemuOpts >>>>> with a single option 'type' with value '{"id":"foo"}'. The error >>>>> message comes from the opts visitor. >>>>> >>>>> To fix this, make netdev_is_modern() return true when optarg[0] == '{'. >>>>> This matches how qobject_input_visitor_new_str() recognizes JSON. >>>> >>>> OK >>>> >>>>> >>>>> Issue 2: when keyval_parse() detects an error, we throw it away and fall >>>>> back to QemuOpts. This is commonly what we want. But not always. For >>>>> instance: >>>>> >>>>> $ qemu-system-x86_64 -netdev 'type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off' >>>>> >>>>> Note the typo "ipv4-off" instead of ipv4=off. The error reporting is crap: >>>>> >>>>> qemu-system-x86_64: -netdev type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off: warning: short-form boolean option 'addr.ipv4-off' deprecated >>>>> Please use addr.ipv4-off=on instead >>>>> qemu-system-x86_64: -netdev type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off: Parameter 'type' is missing >>>>> >>>>> We get this because netdev_is_modern() guesses wrongly: keyval_parse() >>>>> fails with the perfectly reasonable error message "Expected '=' after >>>>> parameter 'addr.ipv4-off'", but netdev_is_modern() ignores the error, >>>>> and fails. We fall back to QemuOpts, and confusion ensues. >>>>> >>>>> I'm not sure we can do much better with reasonable effort. If we decide >>>>> to accept this behavior, it should be documented at least in the source >>>>> code. >>>> >>>> What about using modern syntax by default? >>>> >>>> args = keyval_parse(optarg, "type", NULL, NULL); >>>> if (!args) { >>>> /* cannot detect the syntax, use new style syntax */ >>>> return true; >>>> } >>> >>> As is, netdev_is_modern() has three cases: >>> >>> 1. keyval_parse() fails >>> >>> 2. keyval_parse() succeeds, but value of @type is not modern >>> >>> 3. keyval_parse() succeeds, and value of @type is modern >>> >>> In case 3. we're sure, because even if qemu_opts_parse_noisily() also >>> succeeded, it would result in the same value of @type. >>> >>> In case 2, assuming traditional seems reasonable. The assumption can be >>> wrong when the user intends modern, but fat-fingers the type=T part. >>> >>> In case 1, we know nothing. >>> >>> Guessing modern is wrong when the user intends traditional. This >>> happens when a meant-to-be-traditional @optarg also parses as modern. >>> Quite possible. >> >> I don't see why keyval_parse() fails in this case. Any example? > > Brain cramp on my part, I'm afraid %-} Let me start over. > > Guessing modern is wrong when the user intends traditional. Two > sub-cases then: > > * @optarg parses fine as traditional. For instance, > > $ qemu-system-x86_64 -netdev type=user,id=foo,ipv4 > > parses with a warning: > > option 'ipv4' deprecated > Please use ipv4=on instead > > This is how current master behaves. > > Guessing modern makes this fail instead: > > qemu-system-x86_64: -netdev type=user,id=foo,ipv4: Expected '=' after parameter 'ipv4' > > Regression. > > * @optarg fails to parse traditional, too. The error reporting is for > modern even though the user intends traditional. Can be misleading. > Example: > > $ qemu-system-x86_64 -netdev type=user,id=_,ipv4 > > Current master: > > qemu-system-x86_64: -netdev type=user,id=_,ipv4: Parameter 'id' expects an identifier > Identifiers consist of letters, digits, '-', '.', '_', starting with a letter. > > Guessing modern instead: > > qemu-system-x86_64: -netdev type=user,id=_,ipv4: Expected '=' after parameter 'ipv4' > > This should be rare in practice, as traditional parsing detects very > few errors. > >>> Guessing traditional is wrong when the user intends modern. This >>> happens when a meant-to-be-modern @optarg fails to parse as modern, >>> i.e. whenever the user screws up modern syntax. >> >> This one is the example you gave (ipv4-off) > > Two sub-cases then: > > * @optarg parses fine as traditional. The parse result is unlikely to > make sense, though. For instance, > > $ qemu-system-x86_64 -netdev type=stream,id=foo,server > > parses with a warning: > > qemu-system-x86_64: -netdev type=stream,id=foo,server: warning: short-form boolean option 'server' deprecated > Please use server=on instead > > But the result fails in the opts visitor: > > qemu-system-x86_64: -netdev type=stream,id=foo,server: Parameter 'type' is missing > > In this case, we're better off with guessing modern: > > qemu-system-x86_64: -netdev type=stream,id=foo,server: Expected '=' after parameter > > * @optarg fails to parse traditional, too. The error reporting is for > traditional even though the user intends modern. Can be misleading. > > This is my ipv4-off example. > > Can't win. Parsers simply don't compose that way. > >>> Which guess is less bad? I'm not sure. Thoughts? >> >> Perhaps we can simply fail if keyval_parse() fails? >> >> something like: >> >> args = keyval_parse(optarg, "type", NULL, &error_fatal); >> type = qdict_get_try_str(args, "type"); >> return !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); > > This rejects working option arguments that don't also parse as modern, > such as "-netdev type=user,id=foo,ipv4". > > Guessing traditional seems to be the least bad solution so far. > > Supporting both traditional and modern syntax in an option argument is a > swamp. Can we bypass it somehow? > > -object uses traditional QemuOpts parsing for key=value,..., and modern > parsing for JSON. Sticking to traditional sidesteps compatibility > issues. But you have to use JSON for things traditional can't express. > > Thoughts? > I don't want to force user to switch to JSON to ease move from "-netdev socket" to "-netdev stream" and "-netdev dgram". But I need to be able to parse SocketAddress to specify unix and inet socket address. As we want to keep the same behavior on error cases (it's what I understand from your analysis), perhaps we can rely on traditional mechanism to detect the type: qemu_opts_parse()? bool netdev_is_modern(const char *optarg) { QemuOpts *opts; bool is_modern; const char *type; static QemuOptsList dummy_opts = { .name = "netdev", .implied_opt_name = "type", .head = QTAILQ_HEAD_INITIALIZER(dummy_opts.head), .desc = { { } }, }; if (optarg[0] == '{') { return true; } opts = qemu_opts_parse(&dummy_opts, optarg, true, &error_fatal); type = qemu_opt_get(opts, "type"); is_modern = !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); qemu_opts_reset(&dummy_opts); return is_modern; } The errors are directly reported by qemu_opts_parse(..., &error_fatal), and are the ones expected in the traditional case. But the error reported for the modern case are not correct anymore. I really don't think there is a good solution to our problem. We must accept an incorrect error report in one of these cases. To not break existing error report seems to be the way to go (qemu_opt_parse()). Thanks, Laurent
Laurent Vivier <lvivier@redhat.com> writes: > On 22/06/2022 13:47, Markus Armbruster wrote: >> Laurent Vivier <lvivier@redhat.com> writes: >> >>> On 21/06/2022 10:49, Markus Armbruster wrote: >>>> Laurent Vivier <lvivier@redhat.com> writes: >>>> >>>>> On 20/06/2022 17:21, Markus Armbruster wrote: >>>>>> Laurent Vivier <lvivier@redhat.com> writes: >>>>>> >>>>>>> Copied from socket netdev file and modified to use SocketAddress >>>>>>> to be able to introduce new features like unix socket. >>>>>>> >>>>>>> "udp" and "mcast" are squashed into dgram netdev, multicast is detected >>>>>>> according to the IP address type. >>>>>>> "listen" and "connect" modes are managed by stream netdev. An optional >>>>>>> parameter "server" defines the mode (server by default) >>>>>>> >>>>>>> Signed-off-by: Laurent Vivier <lvivier@redhat.com> >>>>>>> Reviewed-by: Stefano Brivio <sbrivio@redhat.com> >>>>>>> --- >>>> >>>> [...] >>>> >>>>>>> diff --git a/net/net.c b/net/net.c >>>>>>> index c337d3d753fe..440957b272ee 100644 >>>>>>> --- a/net/net.c >>>>>>> +++ b/net/net.c >>>>> ... >>>>>>> @@ -1612,7 +1617,19 @@ void net_init_clients(void) >>>>>>> */ >>>>>>> static bool netdev_is_modern(const char *optarg) >>>>>>> { >>>>>>> - return false; >>>>>>> + QDict *args; >>>>>>> + const char *type; >>>>>>> + bool is_modern; >>>>>>> + >>>>>>> + args = keyval_parse(optarg, "type", NULL, NULL); >>>>>>> + if (!args) { >>>>>>> + return false; >>>>>>> + } >>>>>>> + type = qdict_get_try_str(args, "type"); >>>>>>> + is_modern = !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); >>>>>>> + qobject_unref(args); >>>>>>> + >>>>>>> + return is_modern; >>>>>>> } >>>>>> >>>>>> You could use g_autoptr here: >>>>>> >>>>>> g_autoptr(QDict) args = NULL; >>>>>> const char *type; >>>>>> bool is_modern; >>>>>> >>>>>> args = keyval_parse(optarg, "type", NULL, NULL); >>>>>> if (!args) { >>>>>> return false; >>>>>> } >>>>>> type = qdict_get_try_str(args, "type"); >>>>>> return !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); >>>>>> >>>>>> Matter of taste; you decide. >>>>> >>>>> Looks good. We already had some series to convert existing code to g_autoptr(), so it >>>>> seems the way to do. >>>>> >>>>>> >>>>>> Now recall how this function is used: it decides whether to parse the >>>>>> modern way (with qobject_input_visitor_new_str()) or the traditional way >>>>>> (with qemu_opts_parse_noisily()). >>>>>> >>>>>> qemu_opts_parse_noisily() parses into a QemuOpts, for later use with the >>>>>> opts visitor. >>>>>> >>>>>> qobject_input_visitor_new_str() supports both dotted keys and JSON. The >>>>>> former is parsed with keyval_parse(), the latter with >>>>>> qobject_from_json(). It returns the resulting parse tree wrapped in a >>>>>> suitable QAPI input visitor. >>>>>> >>>>>> Issue 1: since we get there only when keyval_parse() succeeds, JSON is >>>>>> unreachable. Reproducer: >>>>>> >>>>>> $ qemu-system-x86_64 -netdev '{"id":"foo"}' >>>>>> upstream-qemu: -netdev {"id":"foo"}: Parameter 'id' is missing >>>>>> >>>>>> This is parsed with qemu_opts_parse_noisily(), resulting in a QemuOpts >>>>>> with a single option 'type' with value '{"id":"foo"}'. The error >>>>>> message comes from the opts visitor. >>>>>> >>>>>> To fix this, make netdev_is_modern() return true when optarg[0] == '{'. >>>>>> This matches how qobject_input_visitor_new_str() recognizes JSON. >>>>> >>>>> OK >>>>> >>>>>> >>>>>> Issue 2: when keyval_parse() detects an error, we throw it away and fall >>>>>> back to QemuOpts. This is commonly what we want. But not always. For >>>>>> instance: >>>>>> >>>>>> $ qemu-system-x86_64 -netdev 'type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off' >>>>>> >>>>>> Note the typo "ipv4-off" instead of ipv4=off. The error reporting is crap: >>>>>> >>>>>> qemu-system-x86_64: -netdev type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off: warning: short-form boolean option 'addr.ipv4-off' deprecated >>>>>> Please use addr.ipv4-off=on instead >>>>>> qemu-system-x86_64: -netdev type=stream,id=foo,addr.type=inet,addr.host=localhost,addr.port=1234,addr.ipv4-off: Parameter 'type' is missing >>>>>> >>>>>> We get this because netdev_is_modern() guesses wrongly: keyval_parse() >>>>>> fails with the perfectly reasonable error message "Expected '=' after >>>>>> parameter 'addr.ipv4-off'", but netdev_is_modern() ignores the error, >>>>>> and fails. We fall back to QemuOpts, and confusion ensues. >>>>>> >>>>>> I'm not sure we can do much better with reasonable effort. If we decide >>>>>> to accept this behavior, it should be documented at least in the source >>>>>> code. >>>>> >>>>> What about using modern syntax by default? >>>>> >>>>> args = keyval_parse(optarg, "type", NULL, NULL); >>>>> if (!args) { >>>>> /* cannot detect the syntax, use new style syntax */ >>>>> return true; >>>>> } >>>> >>>> As is, netdev_is_modern() has three cases: >>>> >>>> 1. keyval_parse() fails >>>> >>>> 2. keyval_parse() succeeds, but value of @type is not modern >>>> >>>> 3. keyval_parse() succeeds, and value of @type is modern >>>> >>>> In case 3. we're sure, because even if qemu_opts_parse_noisily() also >>>> succeeded, it would result in the same value of @type. >>>> >>>> In case 2, assuming traditional seems reasonable. The assumption can be >>>> wrong when the user intends modern, but fat-fingers the type=T part. >>>> >>>> In case 1, we know nothing. >>>> >>>> Guessing modern is wrong when the user intends traditional. This >>>> happens when a meant-to-be-traditional @optarg also parses as modern. >>>> Quite possible. >>> >>> I don't see why keyval_parse() fails in this case. Any example? >> >> Brain cramp on my part, I'm afraid %-} Let me start over. >> >> Guessing modern is wrong when the user intends traditional. Two >> sub-cases then: >> >> * @optarg parses fine as traditional. For instance, >> >> $ qemu-system-x86_64 -netdev type=user,id=foo,ipv4 >> >> parses with a warning: >> >> option 'ipv4' deprecated >> Please use ipv4=on instead >> >> This is how current master behaves. >> >> Guessing modern makes this fail instead: >> >> qemu-system-x86_64: -netdev type=user,id=foo,ipv4: Expected '=' after parameter 'ipv4' >> >> Regression. >> >> * @optarg fails to parse traditional, too. The error reporting is for >> modern even though the user intends traditional. Can be misleading. >> Example: >> >> $ qemu-system-x86_64 -netdev type=user,id=_,ipv4 >> >> Current master: >> >> qemu-system-x86_64: -netdev type=user,id=_,ipv4: Parameter 'id' expects an identifier >> Identifiers consist of letters, digits, '-', '.', '_', starting with a letter. >> >> Guessing modern instead: >> >> qemu-system-x86_64: -netdev type=user,id=_,ipv4: Expected '=' after parameter 'ipv4' >> >> This should be rare in practice, as traditional parsing detects very >> few errors. >> >>>> Guessing traditional is wrong when the user intends modern. This >>>> happens when a meant-to-be-modern @optarg fails to parse as modern, >>>> i.e. whenever the user screws up modern syntax. >>> >>> This one is the example you gave (ipv4-off) >> >> Two sub-cases then: >> >> * @optarg parses fine as traditional. The parse result is unlikely to >> make sense, though. For instance, >> >> $ qemu-system-x86_64 -netdev type=stream,id=foo,server >> >> parses with a warning: >> >> qemu-system-x86_64: -netdev type=stream,id=foo,server: warning: short-form boolean option 'server' deprecated >> Please use server=on instead >> >> But the result fails in the opts visitor: >> >> qemu-system-x86_64: -netdev type=stream,id=foo,server: Parameter 'type' is missing >> >> In this case, we're better off with guessing modern: >> >> qemu-system-x86_64: -netdev type=stream,id=foo,server: Expected '=' after parameter >> >> * @optarg fails to parse traditional, too. The error reporting is for >> traditional even though the user intends modern. Can be misleading. >> >> This is my ipv4-off example. >> >> Can't win. Parsers simply don't compose that way. >> >>>> Which guess is less bad? I'm not sure. Thoughts? >>> >>> Perhaps we can simply fail if keyval_parse() fails? >>> >>> something like: >>> >>> args = keyval_parse(optarg, "type", NULL, &error_fatal); >>> type = qdict_get_try_str(args, "type"); >>> return !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); >> >> This rejects working option arguments that don't also parse as modern, >> such as "-netdev type=user,id=foo,ipv4". >> >> Guessing traditional seems to be the least bad solution so far. >> >> Supporting both traditional and modern syntax in an option argument is a >> swamp. Can we bypass it somehow? >> >> -object uses traditional QemuOpts parsing for key=value,..., and modern >> parsing for JSON. Sticking to traditional sidesteps compatibility >> issues. But you have to use JSON for things traditional can't express. >> >> Thoughts? >> > > I don't want to force user to switch to JSON to ease move from "-netdev socket" to > "-netdev stream" and "-netdev dgram". > But I need to be able to parse SocketAddress to specify unix and inet socket address. That's fair. > As we want to keep the same behavior on error cases (it's what I understand from your > analysis), We must not break working usage. We may change how erroneous usage fails, but we should try to avoid bad error reporting. Common errors are more important here. Whether they are in existing or in new options doesn't matter. > perhaps we can rely on traditional mechanism to detect the type: qemu_opts_parse()? > > bool netdev_is_modern(const char *optarg) > { > QemuOpts *opts; > bool is_modern; > const char *type; > static QemuOptsList dummy_opts = { > .name = "netdev", > .implied_opt_name = "type", > .head = QTAILQ_HEAD_INITIALIZER(dummy_opts.head), > .desc = { { } }, > }; > > if (optarg[0] == '{') { > return true; > } > > opts = qemu_opts_parse(&dummy_opts, optarg, true, &error_fatal); > type = qemu_opt_get(opts, "type"); > is_modern = !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); > > qemu_opts_reset(&dummy_opts); > > return is_modern; > } > > The errors are directly reported by qemu_opts_parse(..., &error_fatal), and are the ones > expected in the traditional case. > > But the error reported for the modern case are not correct anymore. > > I really don't think there is a good solution to our problem. > > We must accept an incorrect error report in one of these cases. > > To not break existing error report seems to be the way to go (qemu_opt_parse()). I'm not sure about passing &error_fatal to qemu_opts_parse(). Let's examine the possible errors. qemu_opts_parse() calls opts_parse(). opts_parse() extracts the value of parameter "id" with opts_parse_id(). It then creates a QemuOpts with qemu_opts_create(). Errors: * "Invalid parameter 'id'" is impossible since !dummy_opts.merge_lists. * "Parameter "id" expects an identifier" when value of "id" is not wellformed. * "Duplicate ID 'FOO' for netdev" is impossible since @dummy_opts is always empty. It then parses for real with opts_do_parse(). Errors can only come from opt_validate(): * "Invalid parameter 'FOO'" is impossible since opts_accepts_any(&dummy_opts) * a bunch from qemu_opt_parse(), all impossible since !dummy_opt.desc. Conclusion: can only error out when value of "id" is not wellformed. Detecting this error is counter-productive here, because it masks the value of "type", which is what we're after. Better, I think: use opts_do_parse() directly. Something like opts = qemu_opts_create(&dummy_opts, NULL, false, &error_abort); qemu_opts_do_parse(opts, optarg, dummy_opts.implied_opt_name, &error_abort); type = qemu_opt_get(opts, "type"); Note this cannot fail. It may be unable to extract the value of type if the user messes up badly enough. We then assume traditional syntax. Least bad option so far, I think. For once, QemuOpts having basically no syntax checks comes it handy. I'm surprised.
diff --git a/hmp-commands.hx b/hmp-commands.hx index 564f1de364df..2938d13725cc 100644 --- a/hmp-commands.hx +++ b/hmp-commands.hx @@ -1269,7 +1269,7 @@ ERST { .name = "netdev_add", .args_type = "netdev:O", - .params = "[user|tap|socket|vde|bridge|hubport|netmap|vhost-user" + .params = "[user|tap|socket|stream|dgram|vde|bridge|hubport|netmap|vhost-user" #ifdef CONFIG_VMNET "|vmnet-host|vmnet-shared|vmnet-bridged" #endif diff --git a/net/clients.h b/net/clients.h index c9157789f2ce..ed8bdfff1e7c 100644 --- a/net/clients.h +++ b/net/clients.h @@ -40,6 +40,12 @@ int net_init_hubport(const Netdev *netdev, const char *name, int net_init_socket(const Netdev *netdev, const char *name, NetClientState *peer, Error **errp); +int net_init_stream(const Netdev *netdev, const char *name, + NetClientState *peer, Error **errp); + +int net_init_dgram(const Netdev *netdev, const char *name, + NetClientState *peer, Error **errp); + int net_init_tap(const Netdev *netdev, const char *name, NetClientState *peer, Error **errp); diff --git a/net/dgram.c b/net/dgram.c new file mode 100644 index 000000000000..aa4240501ed0 --- /dev/null +++ b/net/dgram.c @@ -0,0 +1,630 @@ +/* + * QEMU System Emulator + * + * Copyright (c) 2003-2008 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "qemu/osdep.h" + +#include "net/net.h" +#include "clients.h" +#include "monitor/monitor.h" +#include "qapi/error.h" +#include "qemu/error-report.h" +#include "qemu/option.h" +#include "qemu/sockets.h" +#include "qemu/iov.h" +#include "qemu/main-loop.h" +#include "qemu/cutils.h" + +typedef struct NetDgramState { + NetClientState nc; + int listen_fd; + int fd; + SocketReadState rs; + /* contains inet host and port destination iff connectionless (SOCK_DGRAM) */ + struct sockaddr_in dgram_dst; + IOHandler *send_fn; /* differs between SOCK_STREAM/SOCK_DGRAM */ + bool read_poll; /* waiting to receive data? */ + bool write_poll; /* waiting to transmit data? */ +} NetDgramState; + +static void net_dgram_accept(void *opaque); +static void net_dgram_writable(void *opaque); + +static void net_dgram_update_fd_handler(NetDgramState *s) +{ + qemu_set_fd_handler(s->fd, + s->read_poll ? s->send_fn : NULL, + s->write_poll ? net_dgram_writable : NULL, + s); +} + +static void net_dgram_read_poll(NetDgramState *s, bool enable) +{ + s->read_poll = enable; + net_dgram_update_fd_handler(s); +} + +static void net_dgram_write_poll(NetDgramState *s, bool enable) +{ + s->write_poll = enable; + net_dgram_update_fd_handler(s); +} + +static void net_dgram_writable(void *opaque) +{ + NetDgramState *s = opaque; + + net_dgram_write_poll(s, false); + + qemu_flush_queued_packets(&s->nc); +} + +static ssize_t net_dgram_receive_dgram(NetClientState *nc, + const uint8_t *buf, size_t size) +{ + NetDgramState *s = DO_UPCAST(NetDgramState, nc, nc); + ssize_t ret; + + do { + if (s->dgram_dst.sin_family != AF_UNIX) { + ret = sendto(s->fd, buf, size, 0, + (struct sockaddr *)&s->dgram_dst, + sizeof(s->dgram_dst)); + } else { + ret = send(s->fd, buf, size, 0); + } + } while (ret == -1 && errno == EINTR); + + if (ret == -1 && errno == EAGAIN) { + net_dgram_write_poll(s, true); + return 0; + } + return ret; +} + +static void net_dgram_send_completed(NetClientState *nc, ssize_t len) +{ + NetDgramState *s = DO_UPCAST(NetDgramState, nc, nc); + + if (!s->read_poll) { + net_dgram_read_poll(s, true); + } +} + +static void net_dgram_rs_finalize(SocketReadState *rs) +{ + NetDgramState *s = container_of(rs, NetDgramState, rs); + + if (qemu_send_packet_async(&s->nc, rs->buf, + rs->packet_len, + net_dgram_send_completed) == 0) { + net_dgram_read_poll(s, false); + } +} + +static void net_dgram_send(void *opaque) +{ + NetDgramState *s = opaque; + int size; + int ret; + uint8_t buf1[NET_BUFSIZE]; + const uint8_t *buf; + + size = recv(s->fd, buf1, sizeof(buf1), 0); + if (size < 0) { + if (errno != EWOULDBLOCK) { + goto eoc; + } + } else if (size == 0) { + /* end of connection */ + eoc: + net_dgram_read_poll(s, false); + net_dgram_write_poll(s, false); + if (s->listen_fd != -1) { + qemu_set_fd_handler(s->listen_fd, net_dgram_accept, NULL, s); + } + closesocket(s->fd); + + s->fd = -1; + net_socket_rs_init(&s->rs, net_dgram_rs_finalize, false); + s->nc.link_down = true; + memset(s->nc.info_str, 0, sizeof(s->nc.info_str)); + + return; + } + buf = buf1; + + ret = net_fill_rstate(&s->rs, buf, size); + + if (ret == -1) { + goto eoc; + } +} + +static void net_dgram_send_dgram(void *opaque) +{ + NetDgramState *s = opaque; + int size; + + size = recv(s->fd, s->rs.buf, sizeof(s->rs.buf), 0); + if (size < 0) { + return; + } + if (size == 0) { + /* end of connection */ + net_dgram_read_poll(s, false); + net_dgram_write_poll(s, false); + return; + } + if (qemu_send_packet_async(&s->nc, s->rs.buf, size, + net_dgram_send_completed) == 0) { + net_dgram_read_poll(s, false); + } +} + +static int net_dgram_mcast_create(struct sockaddr_in *mcastaddr, + struct in_addr *localaddr, + Error **errp) +{ + struct ip_mreq imr; + int fd; + int val, ret; +#ifdef __OpenBSD__ + unsigned char loop; +#else + int loop; +#endif + + if (!IN_MULTICAST(ntohl(mcastaddr->sin_addr.s_addr))) { + error_setg(errp, "specified mcastaddr %s (0x%08x) " + "does not contain a multicast address", + inet_ntoa(mcastaddr->sin_addr), + (int)ntohl(mcastaddr->sin_addr.s_addr)); + return -1; + } + + fd = qemu_socket(PF_INET, SOCK_DGRAM, 0); + if (fd < 0) { + error_setg_errno(errp, errno, "can't create datagram socket"); + return -1; + } + + /* + * Allow multiple sockets to bind the same multicast ip and port by setting + * SO_REUSEADDR. This is the only situation where SO_REUSEADDR should be set + * on windows. Use socket_set_fast_reuse otherwise as it sets SO_REUSEADDR + * only on posix systems. + */ + val = 1; + ret = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val)); + if (ret < 0) { + error_setg_errno(errp, errno, "can't set socket option SO_REUSEADDR"); + goto fail; + } + + ret = bind(fd, (struct sockaddr *)mcastaddr, sizeof(*mcastaddr)); + if (ret < 0) { + error_setg_errno(errp, errno, "can't bind ip=%s to socket", + inet_ntoa(mcastaddr->sin_addr)); + goto fail; + } + + /* Add host to multicast group */ + imr.imr_multiaddr = mcastaddr->sin_addr; + if (localaddr) { + imr.imr_interface = *localaddr; + } else { + imr.imr_interface.s_addr = htonl(INADDR_ANY); + } + + ret = setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, + &imr, sizeof(struct ip_mreq)); + if (ret < 0) { + error_setg_errno(errp, errno, + "can't add socket to multicast group %s", + inet_ntoa(imr.imr_multiaddr)); + goto fail; + } + + /* Force mcast msgs to loopback (eg. several QEMUs in same host */ + loop = 1; + ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, + &loop, sizeof(loop)); + if (ret < 0) { + error_setg_errno(errp, errno, + "can't force multicast message to loopback"); + goto fail; + } + + /* If a bind address is given, only send packets from that address */ + if (localaddr != NULL) { + ret = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, + localaddr, sizeof(*localaddr)); + if (ret < 0) { + error_setg_errno(errp, errno, + "can't set the default network send interface"); + goto fail; + } + } + + qemu_socket_set_nonblock(fd); + return fd; +fail: + if (fd >= 0) { + closesocket(fd); + } + return -1; +} + +static void net_dgram_cleanup(NetClientState *nc) +{ + NetDgramState *s = DO_UPCAST(NetDgramState, nc, nc); + if (s->fd != -1) { + net_dgram_read_poll(s, false); + net_dgram_write_poll(s, false); + close(s->fd); + s->fd = -1; + } + if (s->listen_fd != -1) { + qemu_set_fd_handler(s->listen_fd, NULL, NULL, NULL); + closesocket(s->listen_fd); + s->listen_fd = -1; + } +} + +static NetClientInfo net_dgram_socket_info = { + .type = NET_CLIENT_DRIVER_DGRAM, + .size = sizeof(NetDgramState), + .receive = net_dgram_receive_dgram, + .cleanup = net_dgram_cleanup, +}; + +static NetDgramState *net_dgram_fd_init_dgram(NetClientState *peer, + const char *model, + const char *name, + int fd, int is_fd, + SocketAddress *mcast, + Error **errp) +{ + struct sockaddr_in saddr; + int newfd; + NetClientState *nc; + NetDgramState *s; + SocketAddress *sa; + SocketAddressType sa_type; + + sa = socket_local_address(fd, errp); + if (!sa) { + return NULL; + } + sa_type = sa->type; + qapi_free_SocketAddress(sa); + + /* + * fd passed: multicast: "learn" dgram_dst address from bound address and + * save it. Because this may be "shared" socket from a "master" process, + * datagrams would be recv() by ONLY ONE process: we must "clone" this + * dgram socket --jjo + */ + + if (is_fd && mcast != NULL) { + if (convert_host_port(&saddr, mcast->u.inet.host, + mcast->u.inet.port, errp) < 0) { + goto err; + } + /* must be bound */ + if (saddr.sin_addr.s_addr == 0) { + error_setg(errp, "can't setup multicast destination address"); + goto err; + } + /* clone dgram socket */ + newfd = net_dgram_mcast_create(&saddr, NULL, errp); + if (newfd < 0) { + goto err; + } + /* clone newfd to fd, close newfd */ + dup2(newfd, fd); + close(newfd); + + } + + nc = qemu_new_net_client(&net_dgram_socket_info, peer, model, name); + + s = DO_UPCAST(NetDgramState, nc, nc); + + s->fd = fd; + s->listen_fd = -1; + s->send_fn = net_dgram_send_dgram; + net_socket_rs_init(&s->rs, net_dgram_rs_finalize, false); + net_dgram_read_poll(s, true); + + /* mcast: save bound address as dst */ + if (is_fd && mcast != NULL) { + s->dgram_dst = saddr; + snprintf(nc->info_str, sizeof(nc->info_str), + "fd=%d (cloned mcast=%s:%d)", + fd, inet_ntoa(saddr.sin_addr), ntohs(saddr.sin_port)); + } else { + if (sa_type == SOCKET_ADDRESS_TYPE_UNIX) { + s->dgram_dst.sin_family = AF_UNIX; + } + + snprintf(nc->info_str, sizeof(nc->info_str), "fd=%d %s", fd, + SocketAddressType_str(sa_type)); + } + + return s; + +err: + closesocket(fd); + return NULL; +} + +static void net_dgram_connect(void *opaque) +{ + NetDgramState *s = opaque; + s->send_fn = net_dgram_send; + net_dgram_read_poll(s, true); +} + +static void net_dgram_accept(void *opaque) +{ + NetDgramState *s = opaque; + struct sockaddr_in saddr; + socklen_t len; + int fd; + + for (;;) { + len = sizeof(saddr); + fd = qemu_accept(s->listen_fd, (struct sockaddr *)&saddr, &len); + if (fd < 0 && errno != EINTR) { + return; + } else if (fd >= 0) { + qemu_set_fd_handler(s->listen_fd, NULL, NULL, NULL); + break; + } + } + + s->fd = fd; + s->nc.link_down = false; + net_dgram_connect(s); + snprintf(s->nc.info_str, sizeof(s->nc.info_str), "connection from %s:%d", + inet_ntoa(saddr.sin_addr), ntohs(saddr.sin_port)); +} + +static int net_dgram_mcast_init(NetClientState *peer, + const char *model, + const char *name, + SocketAddress *remote, + SocketAddress *local, + Error **errp) +{ + NetDgramState *s; + int fd, ret; + struct sockaddr_in saddr; + + if (remote->type != SOCKET_ADDRESS_TYPE_INET) { + error_setg(errp, "multicast only support inet type"); + return -1; + } + + if (convert_host_port(&saddr, remote->u.inet.host, remote->u.inet.port, + errp) < 0) { + return -1; + } + + if (!local) { + fd = net_dgram_mcast_create(&saddr, NULL, errp); + if (fd < 0) { + return -1; + } + } else { + switch (local->type) { + case SOCKET_ADDRESS_TYPE_INET: { + struct in_addr localaddr; + + if (inet_aton(local->u.inet.host, &localaddr) == 0) { + error_setg(errp, "localaddr '%s' is not a valid IPv4 address", + local->u.inet.host); + return -1; + } + + fd = net_dgram_mcast_create(&saddr, &localaddr, errp); + if (fd < 0) { + return -1; + } + break; + } + case SOCKET_ADDRESS_TYPE_FD: + fd = monitor_fd_param(monitor_cur(), local->u.fd.str, errp); + if (fd == -1) { + return -1; + } + ret = qemu_socket_try_set_nonblock(fd); + if (ret < 0) { + error_setg_errno(errp, -ret, "%s: Can't use file descriptor %d", + name, fd); + return -1; + } + break; + default: + error_setg(errp, "only support inet or fd type for local"); + return -1; + } + } + + s = net_dgram_fd_init_dgram(peer, model, name, fd, + local->type == SOCKET_ADDRESS_TYPE_FD, + remote, errp); + if (!s) { + return -1; + } + + s->dgram_dst = saddr; + + snprintf(s->nc.info_str, sizeof(s->nc.info_str), "mcast=%s:%d", + inet_ntoa(saddr.sin_addr), ntohs(saddr.sin_port)); + return 0; + +} + +static int net_dgram_udp_init(NetClientState *peer, + const char *model, + const char *name, + SocketAddress *remote, + SocketAddress *local, + Error **errp) +{ + NetDgramState *s; + int fd, ret; + struct sockaddr_in raddr_in; + gchar *info_str; + + if (remote) { + if (local->type == SOCKET_ADDRESS_TYPE_FD) { + error_setg(errp, "don't set remote with local.fd"); + return -1; + } + if (remote->type != local->type) { + error_setg(errp, "remote and local types must be the same"); + return -1; + } + } else { + if (local->type != SOCKET_ADDRESS_TYPE_FD) { + error_setg(errp, "type=inet requires remote parameter"); + return -1; + } + } + + switch (local->type) { + case SOCKET_ADDRESS_TYPE_INET: { + struct sockaddr_in laddr_in; + + if (convert_host_port(&laddr_in, local->u.inet.host, local->u.inet.port, + errp) < 0) { + return -1; + } + + if (convert_host_port(&raddr_in, remote->u.inet.host, + remote->u.inet.port, errp) < 0) { + return -1; + } + + fd = qemu_socket(PF_INET, SOCK_DGRAM, 0); + if (fd < 0) { + error_setg_errno(errp, errno, "can't create datagram socket"); + return -1; + } + + ret = socket_set_fast_reuse(fd); + if (ret < 0) { + error_setg_errno(errp, errno, + "can't set socket option SO_REUSEADDR"); + closesocket(fd); + return -1; + } + ret = bind(fd, (struct sockaddr *)&laddr_in, sizeof(laddr_in)); + if (ret < 0) { + error_setg_errno(errp, errno, "can't bind ip=%s to socket", + inet_ntoa(laddr_in.sin_addr)); + closesocket(fd); + return -1; + } + qemu_socket_set_nonblock(fd); + + info_str = g_strdup_printf("udp=%s:%d/%s:%d", + inet_ntoa(laddr_in.sin_addr), ntohs(laddr_in.sin_port), + inet_ntoa(raddr_in.sin_addr), ntohs(raddr_in.sin_port)); + + break; + } + case SOCKET_ADDRESS_TYPE_FD: + fd = monitor_fd_param(monitor_cur(), local->u.fd.str, errp); + if (fd == -1) { + return -1; + } + ret = qemu_socket_try_set_nonblock(fd); + if (ret < 0) { + error_setg_errno(errp, -ret, "%s: Can't use file descriptor %d", + name, fd); + return -1; + } + break; + default: + error_setg(errp, "only support inet or fd type for local"); + return -1; + } + + s = net_dgram_fd_init_dgram(peer, model, name, fd, 0, NULL, errp); + if (!s) { + return -1; + } + + if (remote) { + s->dgram_dst = raddr_in; + + pstrcpy(s->nc.info_str, sizeof(s->nc.info_str), info_str); + g_free(info_str); + } + return 0; +} + +static int net_dgram_init(NetClientState *peer, + const char *model, + const char *name, + SocketAddress *remote, + SocketAddress *local, + Error **errp) +{ + /* detect multicast address */ + if (remote && remote->type == SOCKET_ADDRESS_TYPE_INET) { + struct sockaddr_in mcastaddr; + + if (convert_host_port(&mcastaddr, remote->u.inet.host, + remote->u.inet.port, errp) < 0) { + return -1; + } + + if (IN_MULTICAST(ntohl(mcastaddr.sin_addr.s_addr))) { + return net_dgram_mcast_init(peer, model, name, remote, local, + errp); + } + } + /* unicast address */ + if (!local) { + error_setg(errp, "dgram requires local= parameter"); + return -1; + } + return net_dgram_udp_init(peer, model, name, remote, local, errp); +} + +int net_init_dgram(const Netdev *netdev, const char *name, + NetClientState *peer, Error **errp) +{ + const NetdevDgramOptions *sock; + + assert(netdev->type == NET_CLIENT_DRIVER_DGRAM); + sock = &netdev->u.dgram; + + return net_dgram_init(peer, "dgram", name, sock->remote, sock->local, + errp); +} diff --git a/net/hub.c b/net/hub.c index 1375738bf121..67ca53485638 100644 --- a/net/hub.c +++ b/net/hub.c @@ -313,6 +313,8 @@ void net_hub_check_clients(void) case NET_CLIENT_DRIVER_USER: case NET_CLIENT_DRIVER_TAP: case NET_CLIENT_DRIVER_SOCKET: + case NET_CLIENT_DRIVER_STREAM: + case NET_CLIENT_DRIVER_DGRAM: case NET_CLIENT_DRIVER_VDE: case NET_CLIENT_DRIVER_VHOST_USER: has_host_dev = 1; diff --git a/net/meson.build b/net/meson.build index 754e2d1d405c..896d86d43914 100644 --- a/net/meson.build +++ b/net/meson.build @@ -13,6 +13,8 @@ softmmu_ss.add(files( 'net.c', 'queue.c', 'socket.c', + 'stream.c', + 'dgram.c', 'util.c', )) diff --git a/net/net.c b/net/net.c index c337d3d753fe..440957b272ee 100644 --- a/net/net.c +++ b/net/net.c @@ -48,6 +48,7 @@ #include "qemu/qemu-print.h" #include "qemu/main-loop.h" #include "qemu/option.h" +#include "qemu/keyval.h" #include "qapi/error.h" #include "qapi/opts-visitor.h" #include "sysemu/runstate.h" @@ -1014,6 +1015,8 @@ static int (* const net_client_init_fun[NET_CLIENT_DRIVER__MAX])( #endif [NET_CLIENT_DRIVER_TAP] = net_init_tap, [NET_CLIENT_DRIVER_SOCKET] = net_init_socket, + [NET_CLIENT_DRIVER_STREAM] = net_init_stream, + [NET_CLIENT_DRIVER_DGRAM] = net_init_dgram, #ifdef CONFIG_VDE [NET_CLIENT_DRIVER_VDE] = net_init_vde, #endif @@ -1101,6 +1104,8 @@ void show_netdevs(void) int idx; const char *available_netdevs[] = { "socket", + "stream", + "dgram", "hubport", "tap", #ifdef CONFIG_SLIRP @@ -1612,7 +1617,19 @@ void net_init_clients(void) */ static bool netdev_is_modern(const char *optarg) { - return false; + QDict *args; + const char *type; + bool is_modern; + + args = keyval_parse(optarg, "type", NULL, NULL); + if (!args) { + return false; + } + type = qdict_get_try_str(args, "type"); + is_modern = !g_strcmp0(type, "stream") || !g_strcmp0(type, "dgram"); + qobject_unref(args); + + return is_modern; } /* diff --git a/net/stream.c b/net/stream.c new file mode 100644 index 000000000000..fdc97ee43a56 --- /dev/null +++ b/net/stream.c @@ -0,0 +1,425 @@ +/* + * QEMU System Emulator + * + * Copyright (c) 2003-2008 Fabrice Bellard + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +#include "qemu/osdep.h" + +#include "net/net.h" +#include "clients.h" +#include "monitor/monitor.h" +#include "qapi/error.h" +#include "qemu/error-report.h" +#include "qemu/option.h" +#include "qemu/sockets.h" +#include "qemu/iov.h" +#include "qemu/main-loop.h" +#include "qemu/cutils.h" + +typedef struct NetStreamState { + NetClientState nc; + int listen_fd; + int fd; + SocketReadState rs; + unsigned int send_index; /* number of bytes sent*/ + IOHandler *send_fn; + bool read_poll; /* waiting to receive data? */ + bool write_poll; /* waiting to transmit data? */ +} NetStreamState; + +static void net_stream_accept(void *opaque); +static void net_stream_writable(void *opaque); + +static void net_stream_update_fd_handler(NetStreamState *s) +{ + qemu_set_fd_handler(s->fd, + s->read_poll ? s->send_fn : NULL, + s->write_poll ? net_stream_writable : NULL, + s); +} + +static void net_stream_read_poll(NetStreamState *s, bool enable) +{ + s->read_poll = enable; + net_stream_update_fd_handler(s); +} + +static void net_stream_write_poll(NetStreamState *s, bool enable) +{ + s->write_poll = enable; + net_stream_update_fd_handler(s); +} + +static void net_stream_writable(void *opaque) +{ + NetStreamState *s = opaque; + + net_stream_write_poll(s, false); + + qemu_flush_queued_packets(&s->nc); +} + +static ssize_t net_stream_receive(NetClientState *nc, const uint8_t *buf, + size_t size) +{ + NetStreamState *s = DO_UPCAST(NetStreamState, nc, nc); + uint32_t len = htonl(size); + struct iovec iov[] = { + { + .iov_base = &len, + .iov_len = sizeof(len), + }, { + .iov_base = (void *)buf, + .iov_len = size, + }, + }; + size_t remaining; + ssize_t ret; + + remaining = iov_size(iov, 2) - s->send_index; + ret = iov_send(s->fd, iov, 2, s->send_index, remaining); + + if (ret == -1 && errno == EAGAIN) { + ret = 0; /* handled further down */ + } + if (ret == -1) { + s->send_index = 0; + return -errno; + } + if (ret < (ssize_t)remaining) { + s->send_index += ret; + net_stream_write_poll(s, true); + return 0; + } + s->send_index = 0; + return size; +} + +static void net_stream_send_completed(NetClientState *nc, ssize_t len) +{ + NetStreamState *s = DO_UPCAST(NetStreamState, nc, nc); + + if (!s->read_poll) { + net_stream_read_poll(s, true); + } +} + +static void net_stream_rs_finalize(SocketReadState *rs) +{ + NetStreamState *s = container_of(rs, NetStreamState, rs); + + if (qemu_send_packet_async(&s->nc, rs->buf, + rs->packet_len, + net_stream_send_completed) == 0) { + net_stream_read_poll(s, false); + } +} + +static void net_stream_send(void *opaque) +{ + NetStreamState *s = opaque; + int size; + int ret; + uint8_t buf1[NET_BUFSIZE]; + const uint8_t *buf; + + size = recv(s->fd, buf1, sizeof(buf1), 0); + if (size < 0) { + if (errno != EWOULDBLOCK) { + goto eoc; + } + } else if (size == 0) { + /* end of connection */ + eoc: + net_stream_read_poll(s, false); + net_stream_write_poll(s, false); + if (s->listen_fd != -1) { + qemu_set_fd_handler(s->listen_fd, net_stream_accept, NULL, s); + } + closesocket(s->fd); + + s->fd = -1; + net_socket_rs_init(&s->rs, net_stream_rs_finalize, false); + s->nc.link_down = true; + memset(s->nc.info_str, 0, sizeof(s->nc.info_str)); + + return; + } + buf = buf1; + + ret = net_fill_rstate(&s->rs, buf, size); + + if (ret == -1) { + goto eoc; + } +} + +static void net_stream_cleanup(NetClientState *nc) +{ + NetStreamState *s = DO_UPCAST(NetStreamState, nc, nc); + if (s->fd != -1) { + net_stream_read_poll(s, false); + net_stream_write_poll(s, false); + close(s->fd); + s->fd = -1; + } + if (s->listen_fd != -1) { + qemu_set_fd_handler(s->listen_fd, NULL, NULL, NULL); + closesocket(s->listen_fd); + s->listen_fd = -1; + } +} + +static void net_stream_connect(void *opaque) +{ + NetStreamState *s = opaque; + s->send_fn = net_stream_send; + net_stream_read_poll(s, true); +} + +static NetClientInfo net_stream_info = { + .type = NET_CLIENT_DRIVER_STREAM, + .size = sizeof(NetStreamState), + .receive = net_stream_receive, + .cleanup = net_stream_cleanup, +}; + +static NetStreamState *net_stream_fd_init_stream(NetClientState *peer, + const char *model, + const char *name, + int fd, int is_connected) +{ + NetClientState *nc; + NetStreamState *s; + + nc = qemu_new_net_client(&net_stream_info, peer, model, name); + + snprintf(nc->info_str, sizeof(nc->info_str), "fd=%d", fd); + + s = DO_UPCAST(NetStreamState, nc, nc); + + s->fd = fd; + s->listen_fd = -1; + net_socket_rs_init(&s->rs, net_stream_rs_finalize, false); + + /* Disable Nagle algorithm on TCP sockets to reduce latency */ + socket_set_nodelay(fd); + + if (is_connected) { + net_stream_connect(s); + } else { + qemu_set_fd_handler(s->fd, NULL, net_stream_connect, s); + } + return s; +} + +static void net_stream_accept(void *opaque) +{ + NetStreamState *s = opaque; + struct sockaddr_in saddr; + socklen_t len; + int fd; + + for (;;) { + len = sizeof(saddr); + fd = qemu_accept(s->listen_fd, (struct sockaddr *)&saddr, &len); + if (fd < 0 && errno != EINTR) { + return; + } else if (fd >= 0) { + qemu_set_fd_handler(s->listen_fd, NULL, NULL, NULL); + break; + } + } + + s->fd = fd; + s->nc.link_down = false; + net_stream_connect(s); + snprintf(s->nc.info_str, sizeof(s->nc.info_str), + "connection from %s:%d", + inet_ntoa(saddr.sin_addr), ntohs(saddr.sin_port)); +} + +static int net_stream_server_init(NetClientState *peer, + const char *model, + const char *name, + SocketAddress *addr, + Error **errp) +{ + NetClientState *nc; + NetStreamState *s; + int fd, ret; + + switch (addr->type) { + case SOCKET_ADDRESS_TYPE_INET: { + struct sockaddr_in saddr_in; + + if (convert_host_port(&saddr_in, addr->u.inet.host, addr->u.inet.port, + errp) < 0) { + return -1; + } + + fd = qemu_socket(PF_INET, SOCK_STREAM, 0); + if (fd < 0) { + error_setg_errno(errp, errno, "can't create stream socket"); + return -1; + } + qemu_socket_set_nonblock(fd); + + socket_set_fast_reuse(fd); + + ret = bind(fd, (struct sockaddr *)&saddr_in, sizeof(saddr_in)); + if (ret < 0) { + error_setg_errno(errp, errno, "can't bind ip=%s to socket", + inet_ntoa(saddr_in.sin_addr)); + closesocket(fd); + return -1; + } + break; + } + case SOCKET_ADDRESS_TYPE_UNIX: { + error_setg(errp, "only support inet type"); + return -1; + } + case SOCKET_ADDRESS_TYPE_FD: + fd = monitor_fd_param(monitor_cur(), addr->u.fd.str, errp); + if (fd == -1) { + return -1; + } + ret = qemu_socket_try_set_nonblock(fd); + if (ret < 0) { + error_setg_errno(errp, -ret, "%s: Can't use file descriptor %d", + name, fd); + return -1; + } + break; + default: + g_assert_not_reached(); + } + + ret = listen(fd, 0); + if (ret < 0) { + error_setg_errno(errp, errno, "can't listen on socket"); + closesocket(fd); + return -1; + } + + nc = qemu_new_net_client(&net_stream_info, peer, model, name); + s = DO_UPCAST(NetStreamState, nc, nc); + s->fd = -1; + s->listen_fd = fd; + s->nc.link_down = true; + net_socket_rs_init(&s->rs, net_stream_rs_finalize, false); + + qemu_set_fd_handler(s->listen_fd, net_stream_accept, NULL, s); + return 0; +} + +static int net_stream_client_init(NetClientState *peer, + const char *model, + const char *name, + SocketAddress *addr, + Error **errp) +{ + NetStreamState *s; + int fd, connected, ret; + gchar *info_str; + + switch (addr->type) { + case SOCKET_ADDRESS_TYPE_INET: { + struct sockaddr_in saddr_in; + + if (convert_host_port(&saddr_in, addr->u.inet.host, addr->u.inet.port, + errp) < 0) { + return -1; + } + + fd = qemu_socket(PF_INET, SOCK_STREAM, 0); + if (fd < 0) { + error_setg_errno(errp, errno, "can't create stream socket"); + return -1; + } + qemu_socket_set_nonblock(fd); + + connected = 0; + for (;;) { + ret = connect(fd, (struct sockaddr *)&saddr_in, sizeof(saddr_in)); + if (ret < 0) { + if (errno == EINTR || errno == EWOULDBLOCK) { + /* continue */ + } else if (errno == EINPROGRESS || + errno == EALREADY || + errno == EINVAL) { + break; + } else { + error_setg_errno(errp, errno, "can't connect socket"); + closesocket(fd); + return -1; + } + } else { + connected = 1; + break; + } + } + info_str = g_strdup_printf("connect to %s:%d", + inet_ntoa(saddr_in.sin_addr), + ntohs(saddr_in.sin_port)); + break; + } + case SOCKET_ADDRESS_TYPE_FD: + fd = monitor_fd_param(monitor_cur(), addr->u.fd.str, errp); + if (fd == -1) { + return -1; + } + ret = qemu_socket_try_set_nonblock(fd); + if (ret < 0) { + error_setg_errno(errp, -ret, "%s: Can't use file descriptor %d", + name, fd); + return -1; + } + connected = 1; + info_str = g_strdup_printf("connect to fd %d", fd); + break; + default: + error_setg(errp, "only support inet or fd type"); + return -1; + } + + s = net_stream_fd_init_stream(peer, model, name, fd, connected); + + pstrcpy(s->nc.info_str, sizeof(s->nc.info_str), info_str); + g_free(info_str); + + return 0; +} + +int net_init_stream(const Netdev *netdev, const char *name, + NetClientState *peer, Error **errp) +{ + const NetdevStreamOptions *sock; + + assert(netdev->type == NET_CLIENT_DRIVER_STREAM); + sock = &netdev->u.stream; + + if (!sock->has_server || sock->server) { + return net_stream_server_init(peer, "stream", name, sock->addr, errp); + } + return net_stream_client_init(peer, "stream", name, sock->addr, errp); +} diff --git a/qapi/net.json b/qapi/net.json index d6f7cfd4d656..5f72995b1d24 100644 --- a/qapi/net.json +++ b/qapi/net.json @@ -7,6 +7,7 @@ ## { 'include': 'common.json' } +{ 'include': 'sockets.json' } ## # @set_link: @@ -566,6 +567,37 @@ '*isolated': 'bool' }, 'if': 'CONFIG_VMNET' } +## +# @NetdevStreamOptions: +# +# Configuration info for stream socket netdev +# +# @addr: socket address to listen on (server=true) +# or connect to (server=false) +# @server: create server socket (default: true) +# +# Since: 7.1 +## +{ 'struct': 'NetdevStreamOptions', + 'data': { + 'addr': 'SocketAddress', + '*server': 'bool' } } + +## +# @NetdevDgramOptions: +# +# Configuration info for datagram socket netdev. +# +# @remote: remote address +# @local: local address +# +# Since: 7.1 +## +{ 'struct': 'NetdevDgramOptions', + 'data': { + '*local': 'SocketAddress', + '*remote': 'SocketAddress' } } + ## # @NetClientDriver: # @@ -579,9 +611,9 @@ # @vmnet-bridged since 7.1 ## { 'enum': 'NetClientDriver', - 'data': [ 'none', 'nic', 'user', 'tap', 'l2tpv3', 'socket', 'vde', - 'bridge', 'hubport', 'netmap', 'vhost-user', 'vhost-vdpa', - { 'name': 'vmnet-host', 'if': 'CONFIG_VMNET' }, + 'data': [ 'none', 'nic', 'user', 'tap', 'l2tpv3', 'socket', 'stream', + 'dgram', 'vde', 'bridge', 'hubport', 'netmap', 'vhost-user', + 'vhost-vdpa', { 'name': 'vmnet-host', 'if': 'CONFIG_VMNET' }, { 'name': 'vmnet-shared', 'if': 'CONFIG_VMNET' }, { 'name': 'vmnet-bridged', 'if': 'CONFIG_VMNET' }] } @@ -610,6 +642,8 @@ 'tap': 'NetdevTapOptions', 'l2tpv3': 'NetdevL2TPv3Options', 'socket': 'NetdevSocketOptions', + 'stream': 'NetdevStreamOptions', + 'dgram': 'NetdevDgramOptions', 'vde': 'NetdevVdeOptions', 'bridge': 'NetdevBridgeOptions', 'hubport': 'NetdevHubPortOptions', diff --git a/qemu-options.hx b/qemu-options.hx index 60cf188da429..c87dcdc65ece 100644 --- a/qemu-options.hx +++ b/qemu-options.hx @@ -2727,6 +2727,18 @@ DEF("netdev", HAS_ARG, QEMU_OPTION_netdev, "-netdev socket,id=str[,fd=h][,udp=host:port][,localaddr=host:port]\n" " configure a network backend to connect to another network\n" " using an UDP tunnel\n" + "-netdev stream,id=str[,server=on|off],addr.type=inet,addr.host=host,addr.port=port\n" + "-netdev stream,id=str[,server=on|off],addr.type=fd,addr.str=h\n" + " configure a network backend to connect to another network\n" + " using a socket connection in stream mode.\n" + "-netdev dgram,id=str,remote.type=inet,remote.host=maddr,remote.port=port[,local.type=inet,local.host=addr]\n" + "-netdev dgram,id=str,remote.type=inet,remote.host=maddr,remote.port=port[,local.type=fd,local.str=h]\n" + " configure a network backend to connect to a multicast maddr and port\n" + " use 'local.host=addr' to specify the host address to send packets from\n" + "-netdev dgram,id=str,local.type=inet,local.host=host,local.port=port[,remote.type=inet,remote.host=host,remote.port=port]\n" + "-netdev dgram,id=str,local.type=fd,local.str=h\n" + " configure a network backend to connect to another network\n" + " using an UDP tunnel\n" #ifdef CONFIG_VDE "-netdev vde,id=str[,sock=socketpath][,port=n][,group=groupname][,mode=octalmode]\n" " configure a network backend to connect to port 'n' of a vde switch\n"