conf: parse/format passt-related XML additions

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@redhat.com>
Reviewed-by: Ján Tomko <jtomko@redhat.com>
This commit is contained in:
Laine Stump 2022-11-11 14:43:45 -05:00
parent 63fbe529fc
commit a8ee7ae301
8 changed files with 394 additions and 19 deletions

View File

@ -4775,19 +4775,25 @@ to the interface.
</devices>
...
Userspace SLIRP stack
^^^^^^^^^^^^^^^^^^^^^
Userspace (SLIRP or passt) connection
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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.
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).
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.
::
@ -4803,6 +4809,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
^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -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",
@ -2611,10 +2624,26 @@ 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->ranges);
g_free(pf);
}
void
virDomainNetDefFree(virDomainNetDef *def)
{
size_t i;
if (!def)
return;
@ -2672,6 +2701,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);
@ -2693,6 +2724,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);
}
@ -8995,6 +9030,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);
@ -9008,6 +9051,120 @@ 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)
@ -9398,6 +9555,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) ||
@ -23316,14 +23476,78 @@ static void
virDomainNetBackendFormat(virBuffer *buf,
virDomainNetBackend *backend)
{
g_auto(virBuffer) attrBuf = VIR_BUFFER_INITIALIZER;
if (!(backend->tap || backend->vhost))
return;
if (backend->type) {
virBufferAsprintf(&attrBuf, " type='%s'",
virDomainNetBackendTypeToString(backend->type));
}
virBufferEscapeString(&attrBuf, " tap='%s'", backend->tap);
virBufferEscapeString(&attrBuf, " vhost='%s'", backend->vhost);
virBufferEscapeString(&attrBuf, " logFile='%s'", backend->logFile);
virBufferEscapeString(&attrBuf, " upstream='%s'", backend->upstream);
virXMLFormatElement(buf, "backend", &attrBuf, NULL);
}
virBufferAddLit(buf, "<backend");
virBufferEscapeString(buf, " tap='%s'", backend->tap);
virBufferEscapeString(buf, " vhost='%s'", backend->vhost);
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];
g_auto(virBuffer) attrBuf = VIR_BUFFER_INITIALIZER;
if (range->start) {
virBufferAsprintf(&attrBuf, " start='%u'", range->start);
if (range->end)
virBufferAsprintf(&attrBuf, " end='%u'", range->end);
if (range->to)
virBufferAsprintf(&attrBuf, " to='%u'", range->to);
}
if (range->exclude) {
virBufferAsprintf(&attrBuf, " exclude='%s'",
virTristateBoolTypeToString(range->exclude));
}
virXMLFormatElement(buf, "range", &attrBuf, NULL);
}
}
static int
virDomainNetPortForwardsFormat(virBuffer *buf,
virDomainNetDef *def)
{
size_t i;
if (!def->nPortForwards)
return 0;
for (i = 0; i < def->nPortForwards; i++) {
g_auto(virBuffer) attrBuf = VIR_BUFFER_INITIALIZER;
g_auto(virBuffer) childBuf = VIR_BUFFER_INIT_CHILD(buf);
virDomainNetPortForward *pf = def->portForwards[i];
virBufferAsprintf(&attrBuf, " proto='%s'",
virDomainNetProtoTypeToString(pf->proto));
if (VIR_SOCKET_ADDR_VALID(&pf->address)) {
g_autofree char *ipStr = virSocketAddrFormat(&pf->address);
if (!ipStr)
return -1;
virBufferAsprintf(&attrBuf, " address='%s'", ipStr);
}
virBufferEscapeString(&attrBuf, " dev='%s'", pf->dev);
virDomainNetPortForwardRangesFormat(&childBuf, pf);
virXMLFormatElementEmpty(buf, "portForward", &attrBuf, &childBuf);
}
return 0;
}
@ -23575,6 +23799,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",

View File

@ -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;
@ -3470,6 +3506,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);
@ -4058,6 +4096,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);

View File

@ -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",
_("The 'passt' backend 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",
_("The 'range' of a 'portForward' 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;

View File

@ -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;

View File

@ -551,6 +551,7 @@ virDomainNetIsVirtioModel;
virDomainNetModelTypeFromString;
virDomainNetModelTypeToString;
virDomainNetNotifyActualDevice;
virDomainNetPortForwardFree;
virDomainNetReleaseActualDevice;
virDomainNetRemove;
virDomainNetRemoveByObj;

View File

@ -0,0 +1 @@
../qemuxml2argvdata/net-user-passt.xml

View File

@ -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");