]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add Lua APIs to set Meta tags in protobuf messages
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 16 Jun 2025 09:01:31 +0000 (11:01 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 17 Jun 2025 10:02:30 +0000 (12:02 +0200)
Signed-off-by: Remi Gacogne <remi.gacogne@powerdns.com>
pdns/dnsdistdist/dnsdist-actions-factory.cc
pdns/dnsdistdist/dnsdist-lua-bindings-dnsquestion.cc
pdns/dnsdistdist/dnsdist-lua-ffi-interface.h
pdns/dnsdistdist/dnsdist-lua-ffi.cc
pdns/dnsdistdist/dnsdist-lua-ffi.hh
pdns/dnsdistdist/dnsdist.hh
pdns/dnsdistdist/docs/reference/dq.rst
pdns/dnsdistdist/meson.build
pdns/dnsdistdist/test-dnsdist-lua-ffi.cc
regression-tests.dnsdist/test_Protobuf.py

index 97be48fa5b557d46560f63395057885dd2c2d29e..07e8ea9b19d722fd3135953eb459df80fb0e7edc 100644 (file)
@@ -1637,6 +1637,9 @@ public:
     static thread_local std::string data;
     data.clear();
     message.serialize(data);
+    if (!dnsquestion->d_rawProtobufContent.empty()) {
+      data.insert(data.end(), dnsquestion->d_rawProtobufContent.begin(), dnsquestion->d_rawProtobufContent.end());
+    }
     remoteLoggerQueueData(*d_logger, data);
 
     return Action::None;
@@ -1798,6 +1801,9 @@ public:
     static thread_local std::string data;
     data.clear();
     message.serialize(data);
+    if (!response->d_rawProtobufContent.empty()) {
+      data.insert(data.end(), response->d_rawProtobufContent.begin(), response->d_rawProtobufContent.end());
+    }
     d_logger->queueData(data);
 
     return Action::None;
index 280fd82e404b595972dff7d0f28be85510ed473d..83f1591ad4f6cdee6f6da9b358edb443a3a21f43 100644 (file)
 #include "dnsdist-snmp.hh"
 #include "dnsparser.hh"
 
+#include "protozero.hh"
+
+static void addMetaKeyAndValuesToProtobufContent(DNSQuestion& dnsQuestion, const std::string& key, const LuaArray<boost::variant<int64_t, std::string>>& values)
+{
+#if !defined(DISABLE_PROTOBUF)
+  protozero::pbf_writer pbfWriter{dnsQuestion.d_rawProtobufContent};
+  protozero::pbf_writer pbfMetaWriter{pbfWriter, static_cast<protozero::pbf_tag_type>(pdns::ProtoZero::Message::Field::meta)};
+  pbfMetaWriter.add_string(static_cast<protozero::pbf_tag_type>(pdns::ProtoZero::Message::MetaField::key), key);
+  protozero::pbf_writer pbfMetaValueWriter{pbfMetaWriter, static_cast<protozero::pbf_tag_type>(pdns::ProtoZero::Message::MetaField::value)};
+  for (const auto& value : values) {
+    if (value.second.type() == typeid(std::string)) {
+      pbfMetaValueWriter.add_string(static_cast<protozero::pbf_tag_type>(pdns::ProtoZero::Message::MetaValueField::stringVal), boost::get<std::string>(value.second));
+    }
+    else {
+      pbfMetaValueWriter.add_uint64(static_cast<protozero::pbf_tag_type>(pdns::ProtoZero::Message::MetaValueField::intVal), boost::get<int64_t>(value.second));
+    }
+  }
+  pbfMetaValueWriter.commit();
+  pbfMetaWriter.commit();
+#endif /* DISABLE_PROTOBUF */
+}
+
 // NOLINTNEXTLINE(readability-function-cognitive-complexity): this function declares Lua bindings, even with a good refactoring it will likely blow up the threshold
 void setupLuaBindingsDNSQuestion([[maybe_unused]] LuaContext& luaCtx)
 {
@@ -219,6 +241,10 @@ void setupLuaBindingsDNSQuestion([[maybe_unused]] LuaContext& luaCtx)
     return *dnsQuestion.ids.qTag;
   });
 
+  luaCtx.registerFunction<void (DNSQuestion::*)(std::string, LuaArray<boost::variant<int64_t, std::string>>)>("setMetaKey", [](DNSQuestion& dnsQuestion, const std::string& key, const LuaArray<boost::variant<int64_t, std::string>>& values) {
+    addMetaKeyAndValuesToProtobufContent(dnsQuestion, key, values);
+  });
+
   luaCtx.registerFunction<void (DNSQuestion::*)(LuaArray<std::string>)>("setProxyProtocolValues", [](DNSQuestion& dnsQuestion, const LuaArray<std::string>& values) {
     if (!dnsQuestion.proxyProtocolValues) {
       dnsQuestion.proxyProtocolValues = make_unique<std::vector<ProxyProtocolValue>>();
@@ -672,5 +698,10 @@ void setupLuaBindingsDNSQuestion([[maybe_unused]] LuaContext& luaCtx)
   luaCtx.registerFunction<uint8_t (DNSResponse::*)()>("getRestartCount", [](DNSResponse& dnsResponse) {
     return dnsResponse.ids.restartCount;
   });
+
+  luaCtx.registerFunction<void (DNSResponse::*)(std::string, LuaArray<boost::variant<int64_t, std::string>>)>("setMetaKey", [](DNSResponse& dnsResponse, const std::string& key, const LuaArray<boost::variant<int64_t, std::string>>& values) {
+    addMetaKeyAndValuesToProtobufContent(dnsResponse, key, values);
+  });
+
 #endif /* DISABLE_NON_FFI_DQ_BINDINGS */
 }
index b818054b94095b6d2b5c2edb66295aacee04bf39..7d5d8bee76664e15025ce10dff04e12648c1f811 100644 (file)
@@ -306,3 +306,12 @@ void dnsdist_ffi_svc_record_parameters_add_ipv6_hint(dnsdist_ffi_svc_record_para
 void dnsdist_ffi_svc_record_parameters_free(dnsdist_ffi_svc_record_parameters* parameters) __attribute__ ((visibility ("default")));
 
 bool dnsdist_ffi_dnsquestion_generate_svc_response(dnsdist_ffi_dnsquestion_t* dnsQuestion, const dnsdist_ffi_svc_record_parameters** parametersList, size_t parametersListSize, uint32_t ttl) __attribute__ ((visibility ("default")));
+
+/* this function adds a new key to the raw meta buffer. It can only be called with the same key on a given query once, and dnsdist_ffi_dnsquestion_meta_end_key should always be called after values have been added */
+void dnsdist_ffi_dnsquestion_meta_begin_key(dnsdist_ffi_dnsquestion_t* dnsQuestion, const char* key, size_t keyLen) __attribute__ ((visibility ("default")));
+/* this function should never be called if dnsdist_ffi_dnsquestion_meta_begin_key has not been called first */
+void dnsdist_ffi_dnsquestion_meta_add_str_value_to_key(dnsdist_ffi_dnsquestion_t* dnsQuestion, const char* value, size_t valueLen) __attribute__ ((visibility ("default")));
+/* this function should never be called if dnsdist_ffi_dnsquestion_meta_begin_key has not been called first */
+void dnsdist_ffi_dnsquestion_meta_add_int64_value_to_key(dnsdist_ffi_dnsquestion_t* dnsQuestion, int64_t value) __attribute__ ((visibility ("default")));
+/* this function should never be called if dnsdist_ffi_dnsquestion_meta_begin_key has not been called first */
+void dnsdist_ffi_dnsquestion_meta_end_key(dnsdist_ffi_dnsquestion_t* dnsQuestion) __attribute__ ((visibility ("default")));
index 33ed5ea6920247c15d25bbf63f24bbca04d3f952..01838f1343cace207b4d4f79110edddb7df9f969 100644 (file)
@@ -2276,3 +2276,77 @@ void dnsdist_ffi_svc_record_parameters_free(dnsdist_ffi_svc_record_parameters* p
   // NOLINTNEXTLINE(cppcoreguidelines-owning-memory): this is a C API, RAII is not an option
   delete parameters;
 }
+
+void dnsdist_ffi_dnsquestion_meta_begin_key([[maybe_unused]] dnsdist_ffi_dnsquestion_t* dnsQuestion, [[maybe_unused]] const char* key, [[maybe_unused]] size_t keyLen)
+{
+#ifndef DISABLE_PROTOBUF
+  if (dnsQuestion == nullptr || key == nullptr || keyLen == 0) {
+    return;
+  }
+
+  if (dnsQuestion->pbfWriter.valid()) {
+    vinfolog("Error in dnsdist_ffi_dnsquestion_meta_begin_key: the previous key has not been ended");
+    return;
+  }
+
+  dnsQuestion->pbfWriter = protozero::pbf_writer{dnsQuestion->dq->d_rawProtobufContent};
+  dnsQuestion->pbfMetaWriter = protozero::pbf_writer{dnsQuestion->pbfWriter, static_cast<protozero::pbf_tag_type>(pdns::ProtoZero::Message::Field::meta)};
+  dnsQuestion->pbfMetaWriter.add_string(static_cast<protozero::pbf_tag_type>(pdns::ProtoZero::Message::MetaField::key), protozero::data_view(key, keyLen));
+  dnsQuestion->pbfMetaValueWriter = protozero::pbf_writer {dnsQuestion->pbfMetaWriter, static_cast<protozero::pbf_tag_type>(pdns::ProtoZero::Message::MetaField::value)};
+#endif /* DISABLE_PROTOBUF */
+}
+
+void dnsdist_ffi_dnsquestion_meta_add_str_value_to_key([[maybe_unused]] dnsdist_ffi_dnsquestion_t* dnsQuestion, [[maybe_unused]] const char* value, [[maybe_unused]] size_t valueLen)
+{
+#ifndef DISABLE_PROTOBUF
+  if (dnsQuestion == nullptr || value == nullptr || valueLen == 0) {
+    return;
+  }
+
+  if (!dnsQuestion->pbfMetaValueWriter.valid()) {
+    vinfolog("Error in dnsdist_ffi_dnsquestion_meta_add_str_value_to_key: trying to add a value without starting a key");
+    return;
+  }
+
+  dnsQuestion->pbfMetaValueWriter.add_string(static_cast<protozero::pbf_tag_type>(pdns::ProtoZero::Message::MetaValueField::stringVal), protozero::data_view(value, valueLen));
+#endif /* DISABLE_PROTOBUF */
+}
+
+void dnsdist_ffi_dnsquestion_meta_add_int64_value_to_key([[maybe_unused]] dnsdist_ffi_dnsquestion_t* dnsQuestion, [[maybe_unused]] int64_t value)
+{
+#ifndef DISABLE_PROTOBUF
+  if (dnsQuestion == nullptr) {
+    return;
+  }
+
+  if (!dnsQuestion->pbfMetaValueWriter.valid()) {
+    vinfolog("Error in dnsdist_ffi_dnsquestion_meta_add_int64_value_to_key: trying to add a value without starting a key");
+    return;
+  }
+
+  dnsQuestion->pbfMetaValueWriter.add_uint64(static_cast<protozero::pbf_tag_type>(pdns::ProtoZero::Message::MetaValueField::intVal), value);
+#endif /* DISABLE_PROTOBUF */
+}
+
+void dnsdist_ffi_dnsquestion_meta_end_key([[maybe_unused]] dnsdist_ffi_dnsquestion_t* dnsQuestion)
+{
+#ifndef DISABLE_PROTOBUF
+  if (dnsQuestion == nullptr) {
+    return;
+  }
+  if (!dnsQuestion->pbfWriter.valid()) {
+    vinfolog("Error in dnsdist_ffi_dnsquestion_meta_end_key: trying to end a key that has not been started");
+    return;
+  }
+
+  try {
+    /* reset the pbf writer so that the sizes are properly updated */
+    dnsQuestion->pbfMetaValueWriter.commit();
+    dnsQuestion->pbfMetaWriter.commit();
+    dnsQuestion->pbfWriter = protozero::pbf_writer();
+  }
+  catch (const std::exception& exp) {
+    vinfolog("Error in dnsdist_ffi_dnsquestion_meta_end_key: %s", exp.what());
+  }
+#endif /* DISABLE_PROTOBUF */
+}
index 644114d69971128b60649dfc3e332bc6bd67aaae..b2c7110c155c21ad5540efdc56b65c7f3c6e106c 100644 (file)
@@ -22,6 +22,7 @@
 #pragma once
 
 #include "dnsdist.hh"
+#include "protozero.hh"
 
 extern "C"
 {
@@ -64,6 +65,11 @@ struct dnsdist_ffi_dnsquestion_t
   std::unique_ptr<std::vector<dnsdist_ffi_tag_t>> tagsVect;
   std::unique_ptr<std::vector<dnsdist_ffi_proxy_protocol_value_t>> proxyProtocolValuesVect;
   std::unique_ptr<std::unordered_map<std::string, std::string>> httpHeaders;
+#ifndef DISABLE_PROTOBUF
+  protozero::pbf_writer pbfWriter;
+  protozero::pbf_writer pbfMetaWriter;
+  protozero::pbf_writer pbfMetaValueWriter;
+#endif /* DISABLE_PROTOBUF */
 };
 
 // dnsdist_ffi_dnsresponse_t is a lightuserdata
index 2d10591eab6989cdae12b6f819f53c45981d1028..d2a8095bbc9d66c4b89001bea2c9eb06d2dcef7b 100644 (file)
@@ -176,6 +176,9 @@ public:
   InternalQueryState& ids;
   std::unique_ptr<Netmask> ecs{nullptr};
   std::string sni; /* Server Name Indication, if any (DoT or DoH) */
+#if !defined(DISABLE_PROTOBUF)
+  std::string d_rawProtobufContent; /* protobuf-encoded content to add to protobuf messages */
+#endif /* DISABLE_PROTOBUF */
   mutable std::unique_ptr<EDNSOptionViewMap> ednsOptions; /* this needs to be mutable because it is parsed just in time, when DNSQuestion is read-only */
   std::shared_ptr<IncomingTCPConnectionState> d_incomingTCPState{nullptr};
   std::unique_ptr<std::vector<ProxyProtocolValue>> proxyProtocolValues{nullptr};
index 6fa21083cd72e66643e63760f3416c6b5365cd95..50b82562fd2d15967cb5a93ac841ffc675028462 100644 (file)
@@ -321,6 +321,15 @@ This state can be modified from the various hooks.
     :param string body: The body of the HTTP response, or a URL if the status code is a redirect (3xx)
     :param string contentType: The HTTP Content-Type header to return for a 200 response, ignored otherwise. Default is ``application/dns-message``.
 
+  .. method:: DNSQuestion:setMetaKey(key, values)
+
+    .. versionadded:: 2.0.0
+
+    Set a meta-data entry to be exported in the ``meta`` field of ProtoBuf messages.
+
+    :param string key: The key
+    :param list values: A list containing strings, integers, or a mix of integers and strings
+
   .. method:: DNSQuestion:setNegativeAndAdditionalSOA(nxd, zone, ttl, mname, rname, serial, refresh, retry, expire, minimum)
 
     .. versionadded:: 1.5.0
index a053232f5b44a0c3d19d4f69d70a339cc418a9d1..644e70267135df4fea9b77531051bd3ff409027f 100644 (file)
@@ -553,6 +553,7 @@ if get_option('unit-tests')
           dep_boost,
           dep_boost_test,
           dep_lua,
+          dep_protozero,
       ],
     )
   )
@@ -572,6 +573,7 @@ if get_option('unit-tests')
           dep_boost_test,
           dep_ffi_interface,
           dep_lua,
+          dep_protozero,
         ],
     }
   }
index b4b2333a3c4efe4c2d34423dc85386b9eb6b4507..ca49f6453df1fd91f6e732fb0d95cacedb28d375 100644 (file)
@@ -28,6 +28,7 @@
 #include <boost/test/unit_test.hpp>
 
 #include "dnsdist-lua-ffi.hh"
+#include "base64.hh"
 #include "dnsdist-cache.hh"
 #include "dnsdist-configuration.hh"
 #include "dnsdist-rings.hh"
@@ -1033,4 +1034,60 @@ BOOST_AUTO_TEST_CASE(test_SVC_Generation)
   dnsdist_ffi_svc_record_parameters_free(parameters);
 }
 
+#if !defined(DISABLE_PROTOBUF)
+BOOST_AUTO_TEST_CASE(test_meta_values)
+{
+
+  InternalQueryState ids;
+  ids.origRemote = ComboAddress("192.0.2.1:4242");
+  ids.origDest = ComboAddress("192.0.2.255:53");
+  ids.qtype = QType::A;
+  ids.qclass = QClass::IN;
+  ids.protocol = dnsdist::Protocol::DoUDP;
+  ids.qname = DNSName("www.powerdns.com.");
+  ids.queryRealTime.start();
+  PacketBuffer query;
+  GenericDNSPacketWriter<PacketBuffer> pwQ(query, ids.qname, QType::A, QClass::IN, 0);
+  pwQ.getHeader()->rd = 1;
+  pwQ.getHeader()->id = htons(42);
+
+  DNSQuestion dnsQuestion(ids, query);
+  dnsdist_ffi_dnsquestion_t lightDQ(&dnsQuestion);
+
+  {
+    /* check invalid parameters */
+    dnsdist_ffi_dnsquestion_meta_begin_key(nullptr, nullptr, 0);
+    dnsdist_ffi_dnsquestion_meta_begin_key(&lightDQ, nullptr, 0);
+    dnsdist_ffi_dnsquestion_meta_begin_key(&lightDQ, "some-key", 0);
+    dnsdist_ffi_dnsquestion_meta_add_str_value_to_key(nullptr, nullptr, 0);
+    dnsdist_ffi_dnsquestion_meta_add_str_value_to_key(&lightDQ, nullptr, 0);
+    dnsdist_ffi_dnsquestion_meta_add_str_value_to_key(&lightDQ, "some-str-value", 0);
+    dnsdist_ffi_dnsquestion_meta_add_int64_value_to_key(nullptr, 0);
+    dnsdist_ffi_dnsquestion_meta_end_key(nullptr);
+  }
+
+  {
+    /* trying to end a key that has not been started */
+    dnsdist_ffi_dnsquestion_meta_end_key(&lightDQ);
+  }
+
+  {
+    const std::string key{"some-key"};
+    const std::string value1{"first value"};
+    const std::string value2{"second value"};
+    BOOST_CHECK_EQUAL(dnsQuestion.d_rawProtobufContent.size(), 0U);
+    dnsdist_ffi_dnsquestion_meta_begin_key(&lightDQ, key.data(), key.size());
+    /* we should not be able to begin a new key without ending it first */
+    dnsdist_ffi_dnsquestion_meta_begin_key(&lightDQ, key.data(), key.size());
+    dnsdist_ffi_dnsquestion_meta_add_str_value_to_key(&lightDQ, value1.data(), value1.size());
+    dnsdist_ffi_dnsquestion_meta_add_int64_value_to_key(&lightDQ, 42);
+    dnsdist_ffi_dnsquestion_meta_add_str_value_to_key(&lightDQ, value2.data(), value2.size());
+    dnsdist_ffi_dnsquestion_meta_add_int64_value_to_key(&lightDQ, -42);
+    dnsdist_ffi_dnsquestion_meta_end_key(&lightDQ);
+    BOOST_CHECK_EQUAL(dnsQuestion.d_rawProtobufContent.size(), 55U);
+    BOOST_CHECK_EQUAL(Base64Encode(dnsQuestion.d_rawProtobufContent), "sgE0Cghzb21lLWtleRIoCgtmaXJzdCB2YWx1ZRAqCgxzZWNvbmQgdmFsdWUQ1v//////////AQ==");
+  }
+}
+#endif /* DISABLE_PROTOBUF */
+
 BOOST_AUTO_TEST_SUITE_END();
index 77cc97d9417643bd1dd40c5f85c7b94bf75155c7..f5717ad3afd56c9ba4d5974908f819e03e1561f5 100644 (file)
@@ -421,9 +421,38 @@ class TestProtobufMetaTags(DNSDistProtobufTest):
     newServer{address="127.0.0.1:%s"}
     rl = newRemoteLogger('127.0.0.1:%d')
 
+    local ffi = require("ffi")
+    local C = ffi.C
+    function add_meta(dq)
+      local key = "my-meta-key-1"
+      local key2 = "my-meta-key-2"
+      C.dnsdist_ffi_dnsquestion_meta_begin_key(dq, key, #key)
+      C.dnsdist_ffi_dnsquestion_meta_add_str_value_to_key(dq, "test", 4)
+      C.dnsdist_ffi_dnsquestion_meta_add_int64_value_to_key(dq, -42)
+      C.dnsdist_ffi_dnsquestion_meta_add_str_value_to_key(dq, "test2", 5)
+      C.dnsdist_ffi_dnsquestion_meta_end_key(dq)
+
+      local key2 = "my-meta-key-2"
+      C.dnsdist_ffi_dnsquestion_meta_begin_key(dq, key2, #key2)
+      C.dnsdist_ffi_dnsquestion_meta_add_str_value_to_key(dq, "foo", 3)
+      C.dnsdist_ffi_dnsquestion_meta_add_int64_value_to_key(dq, 42)
+      C.dnsdist_ffi_dnsquestion_meta_add_str_value_to_key(dq, "bar", 3)
+      C.dnsdist_ffi_dnsquestion_meta_end_key(dq)
+
+      return DNSAction.None
+    end
+
+    function addMetaToResponse(dr)
+      dr:setMetaKey('my-meta-key-1', {'test', -42, 'test2'})
+      dr:setMetaKey('my-meta-key-2', {'foo', 42, 'bar'})
+      return DNSResponseAction.None
+    end
+
+    addAction(AllRule(), LuaFFIAction(add_meta))
     addAction(AllRule(), SetTagAction('my-tag-key', 'my-tag-value'))
     addAction(AllRule(), SetTagAction('my-empty-key', ''))
     addAction(AllRule(), RemoteLogAction(rl, nil, {serverID='dnsdist-server-1', exportTags='*'}, {b64='b64-content', ['my-tag-export-name']='tag:my-tag-key'}))
+    addResponseAction(AllRule(), LuaResponseAction(addMetaToResponse))
     addResponseAction(AllRule(), SetTagResponseAction('my-tag-key2', 'my-tag-value2'))
     addResponseAction(AllRule(), RemoteLogResponseAction(rl, nil, false, {serverID='dnsdist-server-1', exportTags='my-empty-key,my-tag-key2'}, {['my-tag-export-name']='tags'}))
     """
@@ -462,7 +491,7 @@ class TestProtobufMetaTags(DNSDistProtobufTest):
         self.assertIn('my-tag-key:my-tag-value', msg.response.tags)
         self.assertIn('my-empty-key', msg.response.tags)
         # meta tags
-        self.assertEqual(len(msg.meta), 2)
+        self.assertEqual(len(msg.meta), 4)
         tags = {}
         for entry in msg.meta:
             tags[entry.key] = entry.value.stringVal
@@ -470,6 +499,18 @@ class TestProtobufMetaTags(DNSDistProtobufTest):
         self.assertIn('b64', tags)
         self.assertIn('my-tag-export-name', tags)
 
+        self.assertEqual(msg.meta[2].key, 'my-meta-key-1')
+        self.assertEqual(len(msg.meta[2].value.stringVal), 2)
+        self.assertIn('test', msg.meta[2].value.stringVal)
+        self.assertIn('test2', msg.meta[2].value.stringVal)
+        self.assertIn(-42, msg.meta[2].value.intVal)
+
+        self.assertEqual(msg.meta[3].key, 'my-meta-key-2')
+        self.assertEqual(len(msg.meta[3].value.stringVal), 2)
+        self.assertIn('foo', msg.meta[3].value.stringVal)
+        self.assertIn('bar', msg.meta[3].value.stringVal)
+        self.assertIn(42, msg.meta[3].value.intVal)
+
         b64EncodedQuery = base64.b64encode(query.to_wire()).decode('ascii')
         self.assertEqual(tags['b64'], [b64EncodedQuery])
         self.assertEqual(tags['my-tag-export-name'], ['my-tag-value'])
@@ -482,7 +523,7 @@ class TestProtobufMetaTags(DNSDistProtobufTest):
         self.assertIn('my-tag-key2:my-tag-value2', msg.response.tags)
         self.assertIn('my-empty-key', msg.response.tags)
         # meta tags
-        self.assertEqual(len(msg.meta), 1)
+        self.assertEqual(len(msg.meta), 3)
         self.assertEqual(msg.meta[0].key, 'my-tag-export-name')
         self.assertEqual(len(msg.meta[0].value.stringVal), 3)
         self.assertIn('my-tag-key:my-tag-value', msg.meta[0].value.stringVal)
@@ -490,6 +531,18 @@ class TestProtobufMetaTags(DNSDistProtobufTest):
         # no ':' when the value is empty
         self.assertIn('my-empty-key', msg.meta[0].value.stringVal)
 
+        self.assertEqual(msg.meta[1].key, 'my-meta-key-1')
+        self.assertEqual(len(msg.meta[1].value.stringVal), 2)
+        self.assertIn('test', msg.meta[1].value.stringVal)
+        self.assertIn('test2', msg.meta[1].value.stringVal)
+        self.assertIn(-42, msg.meta[1].value.intVal)
+
+        self.assertEqual(msg.meta[2].key, 'my-meta-key-2')
+        self.assertEqual(len(msg.meta[2].value.stringVal), 2)
+        self.assertIn('foo', msg.meta[2].value.stringVal)
+        self.assertIn('bar', msg.meta[2].value.stringVal)
+        self.assertIn(42, msg.meta[2].value.intVal)
+
 class TestProtobufExtendedDNSErrorTags(DNSDistProtobufTest):
     _config_params = ['_testServerPort', '_protobufServerPort']
     _config_template = """