This implements XML config to represent a subset of the features
supported by 'passt' (
https://passt.top), which is an alternative
backend for emulated network devices that requires no elevated
privileges (similar to slirp, but "better").
Along with setting the backend to use passt (via <backend
type='passt'/> when the interface type='user'), we also support
passt's --log-file and --interface options (via the <backend>
subelement logFile and upstream attributes) and its --tcp-ports and
--udp-ports options (which selectively forward incoming connections to
the host on to the guest) via the new <portForward> subelement of
<interface>. Here is an example of the config for a network interface
that uses passt to connect:
<interface type='user'>
<mac address='52:54:00:a8:33:fc'/>
<ip address='192.168.221.122' family='ipv4'/>
<model type='virtio'/>
<backend type='passt' logFile='/tmp/xyzzy.log'
upstream='eth0'/>
<portForward address='10.0.0.1' proto='tcp'
dev='eth0'>
<range start='2022' to='22'/>
<range start='5000' end='5099' to='1000'/>
<range start='5010' end='5029' exclude='yes'/>
</portForward>
<portForward proto='udp'>
<range start='10101'/>
</portForward>
</interface>
In this case:
* the guest will be offered address 192.168.221.122 for its interface
via DHCP
* the passt process will write all log messages to /tmp/xyzzy.log
* routes to the outside for the guest will be derived from the
addresses and routes associated with the host interface "eth0".
* incoming tcp port 2022 to the host will be forwarded to port 22
on the guest.
* incoming tcp ports 5000-5099 (with the exception of ports 5010-5029)
to the host will be forwarded to port 1000-1099 on the guest.
* incoming udp packets on port 10101 will be forwarded (unchanged) to
the guest.
Signed-off-by: Laine Stump <laine(a)redhat.com>
---
docs/formatdomain.rst | 95 +++++++-
src/conf/domain_conf.c | 242 +++++++++++++++++++-
src/conf/domain_conf.h | 40 ++++
src/conf/domain_validate.c | 32 ++-
src/conf/virconftypes.h | 4 +
src/libvirt_private.syms | 1 +
tests/qemuxml2xmloutdata/net-user-passt.xml | 1 +
tests/qemuxml2xmltest.c | 1 +
8 files changed, 401 insertions(+), 15 deletions(-)
create mode 120000 tests/qemuxml2xmloutdata/net-user-passt.xml
diff --git a/docs/formatdomain.rst b/docs/formatdomain.rst
index d7fffc6e0b..cd8fd4483b 100644
--- a/docs/formatdomain.rst
+++ b/docs/formatdomain.rst
@@ -4767,19 +4767,25 @@ to the interface.
</devices>
...
-Userspace SLIRP stack
-^^^^^^^^^^^^^^^^^^^^^
+Userspace (SLIRP or passt) connection
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``user`` type connects the guest interface to the outside via a
+transparent userspace proxy that doesn't require any special system
+privileges, making it usable in cases when libvirt itself is running
+with no privileges (e.g. libvirt's "session mode" daemon, or when
+libvirt is run inside an unprivileged container).
-Provides a virtual LAN with NAT to the outside world. The virtual network has
-DHCP & DNS services and will give the guest VM addresses starting from
-``10.0.2.15``. The default router will be ``10.0.2.2`` and the DNS server will
-be ``10.0.2.3``. This networking is the only option for unprivileged users who
-need their VMs to have outgoing access. :since:`Since 3.8.0` it is possible to
-override the default network address by including an ``ip`` element specifying
-an IPv4 address in its one mandatory attribute, ``address``. Optionally, a
-second ``ip`` element with a ``family`` attribute set to "ipv6" can be
specified
-to add an IPv6 address to the interface. ``address``. Optionally, address
-``prefix`` can be specified.
+By default, this user proxy is done with QEMU's internal SLIRP driver
+which has DHCP & DNS services that give the guest IP addresses
+starting from ``10.0.2.15``, a default route of ``10.0.2.2`` and DNS
+server of ``10.0.2.3``. :since:`Since 3.8.0` it is possible to override
+the default network address by including an ``ip`` element specifying
+an IPv4 address in its one mandatory attribute,
+``address``. Optionally, a second ``ip`` element with a ``family``
+attribute set to "ipv6" can be specified to add an IPv6 address to the
+interface. ``address``. Optionally, address ``prefix`` can be
+specified.
::
@@ -4795,6 +4801,71 @@ to add an IPv6 address to the interface. ``address``. Optionally,
address
</devices>
...
+:since:`Since 9.0.0` an alternate backend implementation of the
+``user`` interface type can be selected by setting the interface's
+``<backend>`` subelement ``type`` attribute to ``passt``. In this
+case, the passt transport (
https://passt.top) is used. Similar to
+SLIRP, passt has an internal DHCP server that provides a requesting
+guest with one ipv4 and one ipv6 address; it then uses userspace
+proxies and a separate network namespace to provide outgoing
+UDP/TCP/ICMP sessions, and optionally redirect incoming traffic
+destined for the host toward the guest instead.
+
+When the passt backend is used, the ``<backend>`` attribute
+``logFile`` can be used to tell the passt process for this interface
+where to write its message log, and the ``<backend>`` attribute
+``upstream`` can tell it to restrict upstream traffic to a particular
+host interface.
+
+Additionally, when passt is used, multiple ``<portForward>`` elements
+can be added to forward incoming network traffic for the host to this
+guest interface. Each ``<portForward>`` must have a ``proto``
+attribute (set to ``tcp`` or ``udp``) and optional original
+``address`` (if not specified, then all incoming sessions to any host
+IP for the given proto/port(s) will be forwarded to the guest).
+
+The decision of which ports to forward is described with zero or more
+``<range>`` subelements of ``<portForward>`` (if there is no
+``<range>`` then **all** ports for the given proto/address will be
+forwarded). Each ``<range>`` has a ``start`` and optional ``end``
+attribute. If ``end`` is omitted then a single port will be forwarded,
+otherwise all ports between ``start`` and ``end`` (inclusive) will be
+forwarded. If the port number(s) should remain unmodified as the
+session is forwarded, no further options are needed, but if the guest
+is expecting the sessions on a different port, then this should be
+specified with the ``to`` attribute of ``<range>`` - the port number
+of each forwarded session in the range will be offeset by "``to`` -
+``start``". A ``<range>`` element can also be used to specify a range
+of ports that should **not** be forwarded. This is done by setting the
+range's ``exclude`` attribute to ``yes``. This may not seem very
+useful, but can be when it is desirable to forward a long range of
+ports **with the exception of some subset**.
+
+::
+
+ ...
+ <devices>
+ ...
+ <interface type='user'>
+ <backend type='passt' logFile='/var/log/passt.log'
upstream='eth0'/>
+ <mac address="00:11:22:33:44:55"/>
+ <ip family='ipv4' address='172.17.2.0'
prefix='24'/>
+ <ip family='ipv6' address='2001:db8:ac10:fd01::'
prefix='64'/>
+ <portForward proto='tcp' address='2001:db8:ac10:fd01::1:10'
start='2022'>
+ <port start='22'/>
+ </portForward>
+ <portForward proto='udp' address='1.2.3.4' start='5000'
end='5020'>
+ <port start='6000' end='6020'/>
+ </portForward>
+ <portForward exclude='yes' proto='tcp'
address='1.2.3.4' start='5010' end='5015'/>
+ <portForward proto='tcp' start='80'/>
+ <portForward proto='tcp' start='443'>
+ <port start='344'/>
+ </portForward>
+ </interface>
+ </devices>
+ ...
+
Generic ethernet connection
^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/src/conf/domain_conf.c b/src/conf/domain_conf.c
index 9502f2ebab..19252b28c5 100644
--- a/src/conf/domain_conf.c
+++ b/src/conf/domain_conf.c
@@ -632,6 +632,19 @@ VIR_ENUM_IMPL(virDomainNetInterfaceLinkState,
"down",
);
+VIR_ENUM_IMPL(virDomainNetBackend,
+ VIR_DOMAIN_NET_BACKEND_LAST,
+ "default",
+ "passt",
+);
+
+VIR_ENUM_IMPL(virDomainNetProto,
+ VIR_DOMAIN_NET_PROTO_LAST,
+ "none",
+ "tcp",
+ "udp",
+);
+
VIR_ENUM_IMPL(virDomainChrDeviceState,
VIR_DOMAIN_CHR_DEVICE_STATE_LAST,
"default",
@@ -2602,10 +2615,25 @@ virDomainNetTeamingInfoFree(virDomainNetTeamingInfo *teaming)
g_free(teaming);
}
+void
+virDomainNetPortForwardFree(virDomainNetPortForward *pf)
+{
+ size_t i;
+
+ if (pf)
+ g_free(pf->dev);
+
+ for (i = 0; i < pf->nRanges; i++)
+ g_free(pf->ranges[i]);
+
+ g_free(pf);
+}
void
virDomainNetDefFree(virDomainNetDef *def)
{
+ size_t i;
+
if (!def)
return;
@@ -2663,6 +2691,8 @@ virDomainNetDefFree(virDomainNetDef *def)
g_free(def->backend.tap);
g_free(def->backend.vhost);
+ g_free(def->backend.logFile);
+ g_free(def->backend.upstream);
virDomainNetTeamingInfoFree(def->teaming);
g_free(def->virtPortProfile);
g_free(def->script);
@@ -2684,6 +2714,10 @@ virDomainNetDefFree(virDomainNetDef *def)
virNetDevBandwidthFree(def->bandwidth);
virNetDevVlanClear(&def->vlan);
+ for (i = 0; i < def->nPortForwards; i++)
+ virDomainNetPortForwardFree(def->portForwards[i]);
+ g_free(def->portForwards);
+
virObjectUnref(def->privateData);
g_free(def);
}
@@ -8977,6 +9011,14 @@ virDomainNetBackendParseXML(xmlNodePtr node,
g_autofree char *tap = virXMLPropString(node, "tap");
g_autofree char *vhost = virXMLPropString(node, "vhost");
+ if (virXMLPropEnum(node, "type", virDomainNetBackendTypeFromString,
+ VIR_XML_PROP_NONZERO, &def->backend.type) < 0) {
+ return -1;
+ }
+
+ def->backend.logFile = virXMLPropString(node, "logFile");
+ def->backend.upstream = virXMLPropString(node, "upstream");
+
if (tap)
def->backend.tap = virFileSanitizePath(tap);
@@ -8990,6 +9032,122 @@ virDomainNetBackendParseXML(xmlNodePtr node,
}
+static virDomainNetPortForwardRange *
+virDomainNetPortForwardRangeParseXML(xmlNodePtr node,
+ xmlXPathContextPtr ctxt)
+{
+ VIR_XPATH_NODE_AUTORESTORE(ctxt)
+ g_autofree virDomainNetPortForwardRange *def = g_new0(virDomainNetPortForwardRange,
1);
+
+ ctxt->node = node;
+
+ if (virXMLPropUInt(node, "start", 10,
+ VIR_XML_PROP_NONZERO, &def->start) < 0) {
+ return NULL;
+ }
+ if (virXMLPropUInt(node, "end", 10,
+ VIR_XML_PROP_NONZERO, &def->end) < 0) {
+ return NULL;
+ }
+ if (virXMLPropUInt(node, "to", 10,
+ VIR_XML_PROP_NONZERO, &def->to) < 0) {
+ return NULL;
+ }
+ if (virXMLPropTristateBool(node, "exclude", VIR_XML_PROP_NONE,
+ &def->exclude) < 0) {
+ return NULL;
+ }
+
+ return g_steal_pointer(&def);
+}
+
+
+static int
+virDomainNetPortForwardRangesParseXML(virDomainNetPortForward *def,
+ xmlXPathContextPtr ctxt)
+{
+ int nRanges;
+ g_autofree xmlNodePtr *ranges = NULL;
+ size_t i;
+
+ if ((nRanges = virXPathNodeSet("./range",
+ ctxt, &ranges)) <= 0) {
+ return nRanges;
+ }
+
+ def->ranges = g_new0(virDomainNetPortForwardRange *, nRanges);
+
+ for (i = 0; i < nRanges; i++) {
+ g_autofree virDomainNetPortForwardRange *range = NULL;
+
+ if (!(range = virDomainNetPortForwardRangeParseXML(ranges[i], ctxt))) {
+ return -1;
+ }
+ def->ranges[def->nRanges++] = g_steal_pointer(&range);
+ }
+ return 0;
+}
+
+
+static virDomainNetPortForward *
+virDomainNetPortForwardDefParseXML(xmlNodePtr node,
+ xmlXPathContextPtr ctxt)
+{
+ VIR_XPATH_NODE_AUTORESTORE(ctxt)
+ g_autofree char *address = NULL;
+ g_autoptr(virDomainNetPortForward) def = g_new0(virDomainNetPortForward, 1);
+
+ ctxt->node = node;
+
+ if (virXMLPropEnum(node, "proto", virDomainNetProtoTypeFromString,
+ VIR_XML_PROP_REQUIRED | VIR_XML_PROP_NONZERO,
+ &def->proto) < 0) {
+ return NULL;
+ }
+
+ address = virXMLPropString(node, "address");
+ if (address && virSocketAddrParse(&def->address, address, AF_UNSPEC)
< 0) {
+ virReportError(VIR_ERR_XML_ERROR,
+ _("Invalid address '%s' in
<portForward>"), address);
+ return NULL;
+ }
+
+ def->dev = virXMLPropString(node, "dev");
+
+ if (virDomainNetPortForwardRangesParseXML(def, ctxt) < 0)
+ return NULL;
+
+ return g_steal_pointer(&def);
+}
+
+
+static int
+virDomainNetPortForwardsParseXML(virDomainNetDef *def,
+ xmlXPathContextPtr ctxt)
+{
+ int nPortForwards;
+ g_autofree xmlNodePtr *portForwards = NULL;
+ size_t i;
+
+ if ((nPortForwards = virXPathNodeSet("./portForward",
+ ctxt, &portForwards)) <= 0) {
+ return nPortForwards;
+ }
+
+ def->portForwards = g_new0(virDomainNetPortForward *, nPortForwards);
+
+ for (i = 0; i < nPortForwards; i++) {
+ g_autoptr(virDomainNetPortForward) pf = NULL;
+
+ if (!(pf = virDomainNetPortForwardDefParseXML(portForwards[i], ctxt))) {
+ return -1;
+ }
+ def->portForwards[def->nPortForwards++] = g_steal_pointer(&pf);
+ }
+ return 0;
+}
+
+
static int
virDomainNetDefParseXMLRequireSource(virDomainNetDef *def,
xmlNodePtr source_node)
@@ -9381,6 +9539,9 @@ virDomainNetDefParseXML(virDomainXMLOption *xmlopt,
ctxt, &def->guestIP) < 0)
return NULL;
+ if (virDomainNetPortForwardsParseXML(def, ctxt) < 0)
+ return NULL;
+
if (def->managed_tap != VIR_TRISTATE_BOOL_NO && def->ifname &&
(flags & VIR_DOMAIN_DEF_PARSE_INACTIVE) &&
(STRPREFIX(def->ifname, VIR_NET_GENERATED_VNET_PREFIX) ||
@@ -23274,17 +23435,91 @@ static void
virDomainNetBackendFormat(virBuffer *buf,
virDomainNetBackend *backend)
{
-
- if (!(backend->tap || backend->vhost))
+ if (!(backend->type || backend->tap || backend->vhost
+ || backend->logFile || backend->upstream)) {
return;
+ }
virBufferAddLit(buf, "<backend");
+ if (backend->type)
+ virBufferAsprintf(buf, " type='%s'",
virDomainNetBackendTypeToString(backend->type));
virBufferEscapeString(buf, " tap='%s'", backend->tap);
virBufferEscapeString(buf, " vhost='%s'", backend->vhost);
+ virBufferEscapeString(buf, " logFile='%s'", backend->logFile);
+ virBufferEscapeString(buf, " upstream='%s'",
backend->upstream);
virBufferAddLit(buf, "/>\n");
}
+static void
+virDomainNetPortForwardRangesFormat(virBuffer *buf,
+ virDomainNetPortForward *def)
+{
+ size_t i;
+
+ for (i = 0; i < def->nRanges; i++) {
+ virDomainNetPortForwardRange *range = def->ranges[i];
+
+ virBufferAddLit(buf, "<range");
+
+ if (range->start) {
+ virBufferAsprintf(buf, " start='%u'", range->start);
+ if (range->end)
+ virBufferAsprintf(buf, " end='%u'", range->end);
+ if (range->to)
+ virBufferAsprintf(buf, " to='%u'", range->to);
+ }
+
+ if (range->exclude) {
+ virBufferAsprintf(buf, " exclude='%s'",
+ virTristateBoolTypeToString(range->exclude));
+ }
+
+ virBufferAddLit(buf, "/>\n");
+ }
+}
+
+
+static int
+virDomainNetPortForwardsFormat(virBuffer *buf,
+ virDomainNetDef *def)
+{
+ size_t i;
+
+ if (!def->nPortForwards)
+ return 0;
+
+ for (i = 0; i < def->nPortForwards; i++) {
+ virDomainNetPortForward *pf = def->portForwards[i];
+
+ virBufferAddLit(buf, "<portForward");
+ virBufferAsprintf(buf, " proto='%s'",
+ virDomainNetProtoTypeToString(pf->proto));
+ if (VIR_SOCKET_ADDR_VALID(&pf->address)) {
+ g_autofree char *ipStr = virSocketAddrFormat(&pf->address);
+
+ if (!ipStr)
+ return -1;
+
+ virBufferAsprintf(buf, " address='%s'", ipStr);
+ }
+ virBufferEscapeString(buf, " dev='%s'", pf->dev);
+
+ if (pf->nRanges == 0) {
+ virBufferAddLit(buf, "/>\n");
+ } else {
+ virBufferAddLit(buf, ">\n");
+ virBufferAdjustIndent(buf, 2);
+ virDomainNetPortForwardRangesFormat(buf, pf);
+ virBufferAdjustIndent(buf, -2);
+ virBufferAddLit(buf, "</portForward>\n");
+ }
+ }
+
+ return 0;
+}
+
+
int
virDomainNetDefFormat(virBuffer *buf,
virDomainNetDef *def,
@@ -23533,6 +23768,9 @@ virDomainNetDefFormat(virBuffer *buf,
if (virDomainNetIPInfoFormat(buf, &def->guestIP) < 0)
return -1;
+ if (virDomainNetPortForwardsFormat(buf, def) < 0)
+ return -1;
+
virBufferEscapeString(buf, "<script path='%s'/>\n",
def->script);
virBufferEscapeString(buf, "<downscript path='%s'/>\n",
diff --git a/src/conf/domain_conf.h b/src/conf/domain_conf.h
index e57e70866a..65fa9cf6e4 100644
--- a/src/conf/domain_conf.h
+++ b/src/conf/domain_conf.h
@@ -1023,6 +1023,21 @@ typedef enum {
VIR_DOMAIN_NET_INTERFACE_LINK_STATE_LAST
} virDomainNetInterfaceLinkState;
+typedef enum {
+ VIR_DOMAIN_NET_BACKEND_DEFAULT = 0,
+ VIR_DOMAIN_NET_BACKEND_PASST,
+
+ VIR_DOMAIN_NET_BACKEND_LAST
+} virDomainNetBackendType;
+
+typedef enum {
+ VIR_DOMAIN_NET_PROTO_NONE = 0,
+ VIR_DOMAIN_NET_PROTO_TCP,
+ VIR_DOMAIN_NET_PROTO_UDP,
+
+ VIR_DOMAIN_NET_PROTO_LAST
+} virDomainNetProto;
+
/* Config that was actually used to bring up interface, after
* resolving network reference. This is private data, only used within
* libvirt, but still must maintain backward compatibility, because
@@ -1052,8 +1067,27 @@ struct _virDomainActualNetDef {
};
struct _virDomainNetBackend {
+ virDomainNetBackendType type;
char *tap;
char *vhost;
+ /* The following are currently only valid/used when backend type='passt' */
+ char *logFile; /* path to logfile used by passt process */
+ char *upstream; /* host interface to use for traffic egress */
+};
+
+struct _virDomainNetPortForwardRange {
+ unsigned int start; /* original dst port range start */
+ unsigned int end; /* range end (0 for "single port") */
+ unsigned int to; /* start of range to forward to (0 for
"unchanged") */
+ virTristateBool exclude; /* true if this is a range to *not* forward */
+};
+
+struct _virDomainNetPortForward {
+ char *dev; /* host interface of incoming traffic */
+ virDomainNetProto proto; /* tcp/udp */
+ virSocketAddr address; /* original dst address (empty = wildcard) */
+ size_t nRanges;
+ virDomainNetPortForwardRange **ranges; /* list of ranges to forward */
};
/* Stores the virtual network interface configuration */
@@ -1159,6 +1193,8 @@ struct _virDomainNetDef {
char *ifname_guest_actual;
char *ifname_guest;
virNetDevIPInfo guestIP;
+ size_t nPortForwards;
+ virDomainNetPortForward **portForwards;
virDomainDeviceInfo info;
char *filter;
GHashTable *filterparams;
@@ -3440,6 +3476,8 @@ void virDomainVsockDefFree(virDomainVsockDef *vsock);
G_DEFINE_AUTOPTR_CLEANUP_FUNC(virDomainVsockDef, virDomainVsockDefFree);
void virDomainNetTeamingInfoFree(virDomainNetTeamingInfo *teaming);
G_DEFINE_AUTOPTR_CLEANUP_FUNC(virDomainNetTeamingInfo, virDomainNetTeamingInfoFree);
+void virDomainNetPortForwardFree(virDomainNetPortForward *pf);
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(virDomainNetPortForward, virDomainNetPortForwardFree);
void virDomainNetDefFree(virDomainNetDef *def);
G_DEFINE_AUTOPTR_CLEANUP_FUNC(virDomainNetDef, virDomainNetDefFree);
void virDomainSmartcardDefFree(virDomainSmartcardDef *def);
@@ -4027,6 +4065,8 @@ VIR_ENUM_DECL(virDomainNetVirtioTxMode);
VIR_ENUM_DECL(virDomainNetMacType);
VIR_ENUM_DECL(virDomainNetTeaming);
VIR_ENUM_DECL(virDomainNetInterfaceLinkState);
+VIR_ENUM_DECL(virDomainNetBackend);
+VIR_ENUM_DECL(virDomainNetProto);
VIR_ENUM_DECL(virDomainNetModel);
VIR_ENUM_DECL(virDomainChrDevice);
VIR_ENUM_DECL(virDomainChrChannelTarget);
diff --git a/src/conf/domain_validate.c b/src/conf/domain_validate.c
index 95b8d9b419..48a701bf93 100644
--- a/src/conf/domain_validate.c
+++ b/src/conf/domain_validate.c
@@ -2134,6 +2134,14 @@ virDomainNetDefValidate(const virDomainNetDef *net)
return -1;
}
+ if (net->type != VIR_DOMAIN_NET_TYPE_USER) {
+ if (net->backend.type == VIR_DOMAIN_NET_BACKEND_PASST) {
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("\"<backend type='passt'/>\"
can only be used with \"<interface type='user'>\""));
+ return -1;
+ }
+ }
+
switch (net->type) {
case VIR_DOMAIN_NET_TYPE_VHOSTUSER:
if (!virDomainNetIsVirtioModel(net)) {
@@ -2150,6 +2158,29 @@ virDomainNetDefValidate(const virDomainNetDef *net)
}
break;
+ case VIR_DOMAIN_NET_TYPE_USER:
+ if (net->backend.type == VIR_DOMAIN_NET_BACKEND_PASST) {
+ size_t p;
+
+ for (p = 0; p < net->nPortForwards; p++) {
+ size_t r;
+ virDomainNetPortForward *pf = net->portForwards[p];
+
+ for (r = 0; r < pf->nRanges; r++) {
+ virDomainNetPortForwardRange *range = pf->ranges[r];
+
+ if (!range->start
+ && (range->end || range->to
+ || range->exclude != VIR_TRISTATE_BOOL_ABSENT)) {
+ virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s",
+ _("<portForward> <range> requires
'start' attribute if 'end', 'to', or 'exclude' is
specified"));
+ return -1;
+ }
+ }
+ }
+ }
+ break;
+
case VIR_DOMAIN_NET_TYPE_NETWORK:
case VIR_DOMAIN_NET_TYPE_VDPA:
case VIR_DOMAIN_NET_TYPE_BRIDGE:
@@ -2162,7 +2193,6 @@ virDomainNetDefValidate(const virDomainNetDef *net)
case VIR_DOMAIN_NET_TYPE_HOSTDEV:
case VIR_DOMAIN_NET_TYPE_VDS:
case VIR_DOMAIN_NET_TYPE_ETHERNET:
- case VIR_DOMAIN_NET_TYPE_USER:
case VIR_DOMAIN_NET_TYPE_NULL:
case VIR_DOMAIN_NET_TYPE_LAST:
break;
diff --git a/src/conf/virconftypes.h b/src/conf/virconftypes.h
index 7bd9aa8e0a..adb2496cba 100644
--- a/src/conf/virconftypes.h
+++ b/src/conf/virconftypes.h
@@ -174,6 +174,10 @@ typedef struct _virDomainNVRAMDef virDomainNVRAMDef;
typedef struct _virDomainNetBackend virDomainNetBackend;
+typedef struct _virDomainNetPortForwardRange virDomainNetPortForwardRange;
+
+typedef struct _virDomainNetPortForward virDomainNetPortForward;
+
typedef struct _virDomainNetDef virDomainNetDef;
typedef struct _virDomainNetTeamingInfo virDomainNetTeamingInfo;
diff --git a/src/libvirt_private.syms b/src/libvirt_private.syms
index ae746a2d51..6a0c1d0972 100644
--- a/src/libvirt_private.syms
+++ b/src/libvirt_private.syms
@@ -551,6 +551,7 @@ virDomainNetIsVirtioModel;
virDomainNetModelTypeFromString;
virDomainNetModelTypeToString;
virDomainNetNotifyActualDevice;
+virDomainNetPortForwardFree;
virDomainNetReleaseActualDevice;
virDomainNetRemove;
virDomainNetRemoveByObj;
diff --git a/tests/qemuxml2xmloutdata/net-user-passt.xml
b/tests/qemuxml2xmloutdata/net-user-passt.xml
new file mode 120000
index 0000000000..cfbc023ada
--- /dev/null
+++ b/tests/qemuxml2xmloutdata/net-user-passt.xml
@@ -0,0 +1 @@
+../qemuxml2argvdata/net-user-passt.xml
\ No newline at end of file
diff --git a/tests/qemuxml2xmltest.c b/tests/qemuxml2xmltest.c
index e13da8bd2c..c9cc3416d5 100644
--- a/tests/qemuxml2xmltest.c
+++ b/tests/qemuxml2xmltest.c
@@ -459,6 +459,7 @@ mymain(void)
DO_TEST_NOCAPS("net-vhostuser");
DO_TEST_NOCAPS("net-user");
DO_TEST_NOCAPS("net-user-addr");
+ DO_TEST_NOCAPS("net-user-passt");
DO_TEST_NOCAPS("net-virtio");
DO_TEST_NOCAPS("net-virtio-device");
DO_TEST_NOCAPS("net-virtio-disable-offloads");
--
2.38.1