diff --git a/README.md b/README.md
index 9209cf4aca73434dbe4fed878d996a1102c59651..dd1b6d18ae9595c094a61e90a42429626afc6663 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-[PACKAGE]: ../../raw/master/packagee-0.1.2-20230706.mkp "package-0.1.2-20230706.mkp"
+[PACKAGE]: ../../raw/master/mkp/unbound-1.2.0-20240421.mkp "unbound-1.2.0-20240421.mkp"
 # Title
 
 A short description about the plugin
diff --git a/mkp/unbound-1.2.0-20240421.mkp b/mkp/unbound-1.2.0-20240421.mkp
new file mode 100644
index 0000000000000000000000000000000000000000..9a1d9420f21bc07886feb932228d2bf5e1a62e90
Binary files /dev/null and b/mkp/unbound-1.2.0-20240421.mkp differ
diff --git a/source/agent_based/unbound.py b/source/agent_based/unbound.py
new file mode 100644
index 0000000000000000000000000000000000000000..4f6e23fc9d5008a30a2d0c9031609a2f2699e3f1
--- /dev/null
+++ b/source/agent_based/unbound.py
@@ -0,0 +1,239 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2022, Jan-Philipp Litza (PLUTEX) <jpl@plutex.de>.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# https://nlnetlabs.nl/projects/unbound/about/
+
+# changes by thl-cmk[at]outlook[dot]com
+# 2024-04-21: removed Union -> no need "int | float" should do
+#             added levels_upper_NOERROR to default parameters -> show up in info line
+
+from typing import (
+    Any,
+    Mapping,
+    # Union,
+)
+
+from cmk.base.plugins.agent_based.agent_based_api.v1.type_defs import (
+    CheckResult,
+    DiscoveryResult,
+    StringTable,
+)
+
+from cmk.base.plugins.agent_based.agent_based_api.v1 import (
+    GetRateError,
+    Service,
+    check_levels,
+    get_rate,
+    get_value_store,
+    register,
+    render,
+)
+
+
+# UnboundSection = Mapping[str, Union[int, float, "UnboundSection"]]
+UnboundSection = Mapping[str, int | float]
+
+
+def render_qps(x: float) -> str:
+    return f'{x:.2f}/s'
+
+
+def parse_unbound(string_table: StringTable) -> UnboundSection:
+    section = {}
+    for key, value in string_table:
+        try:
+            section[key] = int(value)
+        except ValueError:
+            section[key] = float(value)
+    return section
+
+
+register.agent_section(
+    name="unbound",
+    parse_function=parse_unbound,
+)
+
+
+def discover_unbound_cache(section: UnboundSection) -> DiscoveryResult:
+    if 'total.num.cachehits' in section and 'total.num.cachemiss' in section:
+        yield Service()
+
+
+def check_unbound_cache(
+    params: Mapping[str, Any],
+    section: UnboundSection,
+) -> CheckResult:
+    cumulative_cache_hits = section.get('total.num.cachehits')
+    cumulative_cache_miss = section.get('total.num.cachemiss')
+    now = section.get('time.now')
+
+    if None in (cumulative_cache_hits, cumulative_cache_miss, now):
+        return
+
+    cache_hits = get_rate(
+        get_value_store(),
+        'unbound_cache_hits',
+        now,
+        cumulative_cache_hits,
+        raise_overflow=True,
+    )
+    cache_miss = get_rate(
+        get_value_store(),
+        'unbound_cache_miss',
+        now,
+        cumulative_cache_miss,
+        raise_overflow=True,
+    )
+    total = cache_hits + cache_miss
+    hit_perc = (cache_hits / float(total)) * 100.0 if total != 0 else 100.0
+
+    yield from check_levels(
+        value=cache_miss,
+        metric_name="cache_misses_rate",
+        levels_upper=params.get("cache_misses"),
+        render_func=render_qps,
+        label='Cache Misses',
+        notice_only=True,
+    )
+
+    yield from check_levels(
+        value=cache_hits,
+        metric_name="cache_hit_rate",
+        render_func=render_qps,
+        label='Cache Hits',
+        notice_only=True,
+    )
+
+    yield from check_levels(
+        value=hit_perc,
+        metric_name="cache_hit_ratio",
+        levels_lower=params.get("cache_hits"),
+        render_func=render.percent,
+        label='Cache Hit Ratio',
+    )
+
+
+register.check_plugin(
+    name="unbound_cache",
+    service_name="Unbound Cache",
+    sections=["unbound"],
+    discovery_function=discover_unbound_cache,
+    check_function=check_unbound_cache,
+    check_default_parameters={},
+    check_ruleset_name="unbound_cache",
+)
+
+
+def discover_unbound_answers(section: UnboundSection) -> DiscoveryResult:
+    if 'time.now' in section and 'num.answer.rcode.SERVFAIL' in section:
+        yield Service()
+
+
+def check_unbound_answers(params: Mapping, section: UnboundSection) -> CheckResult:
+    key_prefix = 'num.answer.rcode.'
+    if 'time.now' not in section:
+        return
+
+    now = section['time.now']
+
+    total = sum(
+        value for key, value in section.items()
+        if key.startswith(key_prefix)
+    )
+
+    for key, value in section.items():
+        if not key.startswith(key_prefix):
+            continue
+        answer = key[len(key_prefix):]
+
+        try:
+            rate = get_rate(
+                get_value_store(),
+                f'unbound_answers_{answer}',
+                now,
+                value,
+                raise_overflow=True,
+            )
+        except GetRateError:
+            pass
+        else:
+            levels_upper = params.get(f'levels_upper_{answer}')
+            if levels_upper is not None and len(levels_upper) == 3:
+                # levels on the ratio of answers
+                levels_upper = (
+                    levels_upper[0] * total,
+                    levels_upper[1] * total,
+                )
+            yield from check_levels(
+                value=rate,
+                levels_upper=levels_upper,
+                metric_name=f'unbound_answers_{answer}',
+                render_func=render_qps,
+                label=answer,
+                notice_only=f'levels_upper_{answer}' not in params,
+            )
+
+
+register.check_plugin(
+    name="unbound_answers",
+    service_name="Unbound Answers",
+    sections=["unbound"],
+    discovery_function=discover_unbound_answers,
+    check_function=check_unbound_answers,
+    check_default_parameters={
+        'levels_upper_NOERROR': (101,101),
+        'levels_upper_SERVFAIL': (10, 100),
+        'levels_upper_REFUSED': (10, 100),
+    },
+    check_ruleset_name="unbound_answers",
+)
+
+
+def discover_unbound_unwanted_replies(section: UnboundSection) -> DiscoveryResult:
+    if 'time.now' in section and 'unwanted.replies' in section:
+        yield Service()
+
+
+def check_unbound_unwanted_replies(section: UnboundSection) -> CheckResult:
+    if 'time.now' not in section or 'unwanted.replies' not in section:
+        return
+
+    rate = get_rate(
+        get_value_store(),
+        'unbound_unwanted_replies',
+        section['time.now'],
+        section['unwanted.replies'],
+        raise_overflow=True,
+    )
+
+    yield from check_levels(
+        value=rate,
+        levels_upper=(10, 100),
+        metric_name='unbound_unwanted_replies',
+        render_func=render_qps,
+        label='Unwanted Replies',
+    )
+
+
+register.check_plugin(
+    name="unbound_unwanted_replies",
+    service_name="Unbound Unwanted Replies",
+    sections=["unbound"],
+    discovery_function=discover_unbound_unwanted_replies,
+    check_function=check_unbound_unwanted_replies,
+)
diff --git a/source/agents/plugins/unbound b/source/agents/plugins/unbound
new file mode 100755
index 0000000000000000000000000000000000000000..52d336ec0382ce2508c119ccc05a8fc6fee50bdc
--- /dev/null
+++ b/source/agents/plugins/unbound
@@ -0,0 +1,4 @@
+#!/bin/sh
+echo '<<<unbound:sep(61)>>>'
+unbound-control stats_noreset
+echo '<<<>>>'
diff --git a/source/checkman/.gitkeep b/source/checkman/.gitkeep
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/source/checkman/unbound b/source/checkman/unbound
deleted file mode 100644
index 08ef898bcf6eb7a7979de60090d54f9719bbbc5b..0000000000000000000000000000000000000000
--- a/source/checkman/unbound
+++ /dev/null
@@ -1,45 +0,0 @@
-title: Dummy check man page - used as template for new check manuals
-agents: linux, windows, aix, solaris, hpux, vms, freebsd, snmp
-catalog: see modules/catalog.py for possible values
-license: GPL
-distribution: check_mk
-description:
- Describe here: (1) what the check actually does, (2) under which
- circumstances it goes warning/critical, (3) which devices are supported
- by the check, (4) if the check requires a separated plugin or
- tool or separate configuration on the target host.
-
-item:
- Describe the syntax and meaning of the check's item here. Provide all
- information one needs if coding a manual check with {checks +=} in {main.mk}.
- Give an example.  If the check uses {None} as sole item,
- then leave out this section.
-
-examples:
- # Give examples for configuration in {main.mk} here. If the check has
- # configuration variable, then give example for them here.
-
- # set default levels to 40 and 60 percent:
- foo_default_values = (40, 60)
-
- # another configuration variable here:
- inventory_foo_filter = [ "superfoo", "superfoo2" ]
-
-perfdata:
- Describe precisely the number and meaning of performance variables
- the check sends. If it outputs no performance data, then leave out this
- section.
-
-inventory:
- Describe how the inventory for the check works. Which items
- will it find? Describe the influence of check specific
- configuration parameters to the inventory.
-
-[parameters]
-foofirst(int): describe the first parameter here (if parameters are grouped
-        as tuple)
-fooother(string): describe another parameter here.
-
-[configuration]
-foo_default_levels(int, int): Describe global configuration variable of
-    foo here. Important: also tell the user how they are preset.
diff --git a/source/gui/metrics/unbound.py b/source/gui/metrics/unbound.py
new file mode 100644
index 0000000000000000000000000000000000000000..deec35d470fd1f93698eac4e257808afdf9fe597
--- /dev/null
+++ b/source/gui/metrics/unbound.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+# -*- encoding: utf-8; py-indent-offset: 4 -*-
+
+# Copyright (C) 2022, Jan-Philipp Litza (PLUTEX) <jpl@plutex.de>.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# modifications by thl-cmk[at]outlook[dot]com
+# 2024-02-21: changed import path form mk.gui.plugins.metrics to mk.gui.plugins.metrics.utils
+#             added metrics/graph for unbound_unwanted_replies
+#             moved to ~/local/lib/check_mk/gui/plugins/metrics (from local/share/check_mk/web/plugins/metrics)
+#             added perfometer for unbound_answers (NOERROR/SERVFAIL) and unbound_unwanted_replies
+
+from cmk.gui.i18n import _
+
+from cmk.gui.plugins.metrics.utils import (
+    metric_info,
+    graph_info,
+    perfometer_info,
+)
+
+metric_info['unbound_answers_NOERROR'] = {
+    'title': _('Rate of NOERROR answers'),
+    'unit': '1/s',
+    'color': '31/a',
+}
+
+metric_info['unbound_answers_FORMERR'] = {
+    'title': _('Rate of FORMERR answers'),
+    'unit': '1/s',
+    'color': '21/a',
+}
+
+metric_info['unbound_answers_SERVFAIL'] = {
+    'title': _('Rate of SERVFAIL answers'),
+    'unit': '1/s',
+    'color': '11/a',
+}
+
+metric_info['unbound_answers_NXDOMAIN'] = {
+    'title': _('Rate of NXDOMAIN answers'),
+    'unit': '1/s',
+    'color': '51/a',
+}
+
+metric_info['unbound_answers_NOTIMPL'] = {
+    'title': _('Rate of NOTIMPL answers'),
+    'unit': '1/s',
+    'color': '41/a',
+}
+
+metric_info['unbound_answers_REFUSED'] = {
+    'title': _('Rate of REFUSED answers'),
+    'unit': '1/s',
+    'color': '26/a',
+}
+
+metric_info['unbound_answers_nodata'] = {
+    'title': _('Rate of answers without data'),
+    'unit': '1/s',
+    'color': '52/a',
+}
+
+graph_info['unbound_answers'] = {
+    'title': _('Rate of answers'),
+    'metrics': [
+        (f'unbound_answers_{answer}', 'line')
+        for answer in ('NOERROR', 'FORMERR', 'SERVFAIL', 'NXDOMAIN', 'NOTIMPL', 'REFUSED', 'nodata')
+    ],
+}
+
+perfometer_info.append(('stacked', [
+    {
+        'type': 'logarithmic',
+        'metric': 'unbound_answers_NOERROR',
+        'half_value': 100.0,  # ome year
+        'exponent': 2,
+    },
+    {
+        'type': 'logarithmic',
+        'metric': 'unbound_answers_SERVFAIL',
+        'half_value': 50.0,
+        'exponent': 2,
+    },
+]))
+
+metric_info['cache_hit_rate'] = {
+    'title': _('Cache hits per second'),
+    'unit': '1/s',
+    'color': '26/a',
+}
+
+graph_info['cache_hit_misses'] = {
+    'title': _('Cache Hits and Misses'),
+    'metrics': [('cache_hit_rate', 'line'), ('cache_misses_rate', 'line')],
+}
+
+metric_info['unbound_unwanted_replies'] = {
+    'title': _('Unwanted replies'),
+    'unit': '1/s',
+    'color': '26/a',
+}
+
+graph_info['unbound_unwanted_replies'] = {
+    'title': _('Unwanted replies'),
+    'metrics': [('unbound_unwanted_replies', 'area')],
+}
+
+perfometer_info.append({
+    'type': 'logarithmic',
+    'metric': 'unbound_unwanted_replies',
+    'half_value': 100.0,  # ome year
+    'exponent': 2,
+})
diff --git a/source/gui/wato/check_parameters/unbound.py b/source/gui/wato/check_parameters/unbound.py
new file mode 100644
index 0000000000000000000000000000000000000000..5e52a07f2063f156e8af1d1d31604ab6ca7286fb
--- /dev/null
+++ b/source/gui/wato/check_parameters/unbound.py
@@ -0,0 +1,165 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2022, Jan-Philipp Litza (PLUTEX) <jpl@plutex.de>.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+# modifications by thl-cmk[at]outlook[dot]com
+# 2024-04-21: fixed missing FixedValue in import
+#             changed Alternative titles to "Upper levels in qps"/"Upper levels in %" to better differentiate between them
+#             added explicit unit "%2
+#             added ruleset for bakery
+#             renamed to unbound.py (from unbound_parameters.py)
+#             moved to ~/local/lib/check_mk/gui/plugins/wato/check_parameters (from local/share/check_mk/web/plugins/wato)
+#
+
+from cmk.gui.i18n import _
+from cmk.gui.plugins.wato.utils import (
+    CheckParameterRulespecWithoutItem,
+    rulespec_registry,
+    RulespecGroupCheckParametersApplications,
+    HostRulespec,
+)
+from cmk.gui.cee.plugins.wato.agent_bakery.rulespecs.utils import (
+    RulespecGroupMonitoringAgentsAgentPlugins,
+)
+
+from cmk.gui.valuespec import Dictionary, Float, Percentage, Tuple, Alternative, FixedValue
+
+
+def _parameter_valuespec_unbound_cache():
+    return Dictionary(
+        title=_("Unbound: Cache"),
+        elements=[
+            (
+                "cache_misses",
+                Tuple(
+                    title="Levels on cache misses per second",
+                    elements=[
+                        Float(
+                            title="warn",
+                        ),
+                        Float(
+                            title="crit",
+                        ),
+                    ],
+                ),
+            ),
+            (
+                "cache_hits",
+                Tuple(
+                    title="Lower levels for hits in %",
+                    elements=[
+                        Percentage(
+                            title="warn",
+                        ),
+                        Percentage(
+                            title="crit",
+                        ),
+                    ],
+                ),
+            ),
+        ],
+    )
+
+
+rulespec_registry.register(
+    CheckParameterRulespecWithoutItem(
+        check_group_name="unbound_cache",
+        group=RulespecGroupCheckParametersApplications,
+        match_type="dict",
+        parameter_valuespec=_parameter_valuespec_unbound_cache,
+        title=lambda: _("Unbound Cache"),
+    )
+)
+
+
+def _parameter_valuespec_unbound_answers():
+    return Dictionary(
+        elements=[
+            (
+                f"levels_upper_{answer}",
+                Alternative(
+                    title=f'Upper levels for {answer} answers',
+                    show_alternative_title=True,
+                    elements=[
+                        Tuple(
+                            elements=[
+                                Float(title=_("Warning at"), unit=_("qps")),
+                                Float(title=_("Critical at"), unit=_("qps")),
+                            ],
+                            title=f'Upper levels in qps',
+                        ),
+                        Tuple(
+                            elements=[
+                                Percentage(title=_("Warning at"), unit=_("%")),
+                                Percentage(title=_("Critical at"), unit=_("%")),
+                                FixedValue(value="%", totext=""),  # need to decide between both variants
+                            ],
+                            title=f'Upper levels in %',
+                        ),
+                    ]
+                )
+            )
+            for answer in (
+                'NOERROR',
+                'FORMERR',
+                'SERVFAIL',
+                'NXDOMAIN',
+                'NOTIMPL',
+                'REFUSED',
+                'nodata',
+            )
+        ],
+    )
+
+
+rulespec_registry.register(
+    CheckParameterRulespecWithoutItem(
+        check_group_name="unbound_answers",
+        group=RulespecGroupCheckParametersApplications,
+        match_type="dict",
+        parameter_valuespec=_parameter_valuespec_unbound_answers,
+        title=lambda: _("Unbound Answers"),
+    )
+)
+
+
+def _parameter_valuespec_unbound_bakery():
+    return Alternative(
+        title=_('unbound'),
+        elements=[
+            FixedValue(
+                True,
+                title=_('Deploy the unbound agent plugin'),
+                totext=_('The unbound agent plugin will be deployed')
+            ),
+            FixedValue(
+                None,
+                title=_('Do not deploy the unbound agent plugin'),
+                totext=_('The unbound agent plugin will not be deployed')
+            ),
+        ],
+    )
+
+
+rulespec_registry.register(
+    HostRulespec(
+        group=RulespecGroupMonitoringAgentsAgentPlugins,
+        name='agent_config:unbound',
+        valuespec=_parameter_valuespec_unbound_bakery,
+    )
+)
diff --git a/source/lib/python3/cmk/base/cee/plugins/bakery/unbound.py b/source/lib/python3/cmk/base/cee/plugins/bakery/unbound.py
new file mode 100644
index 0000000000000000000000000000000000000000..ac4f50de0799d0302c5b5616f4181436bceec7f4
--- /dev/null
+++ b/source/lib/python3/cmk/base/cee/plugins/bakery/unbound.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2024-04-21
+# File  : share/check_mk/agents/unbound.py
+#
+# bakery unbound plugin
+#
+
+
+from pathlib import Path
+
+from cmk.base.cee.plugins.bakery.bakery_api.v1 import (
+    FileGenerator,
+    OS,
+    Plugin,
+    register
+)
+
+
+def get_unbound_files(conf) -> FileGenerator:
+    if conf is True:
+        yield Plugin(
+            base_os=OS.LINUX,
+            source=Path('unbound'),
+        )
+
+
+register.bakery_plugin(
+    name='unbound',
+    files_function=get_unbound_files,
+)
diff --git a/source/packages/unbound b/source/packages/unbound
new file mode 100644
index 0000000000000000000000000000000000000000..5741833e738d9d625c6d90f3deebeda552341e49
--- /dev/null
+++ b/source/packages/unbound
@@ -0,0 +1,19 @@
+{'author': 'Jan-Philipp Litza <jpl@plutex.de>',
+ 'description': 'Plugin to gather statistics from unbound caching DNS resolver '
+                'via agent plugin. It monitors answer types, cache hit ratio '
+                'and miss rate as well as unwanted reply rate.\n'
+                '\n'
+                'needs in server config:\n'
+                '  server:\n'
+                '      extended-statistics: yes\n',
+ 'download_url': 'https://github.com/PLUTEX/checkmk-unbound/',
+ 'files': {'agent_based': ['unbound.py'],
+           'agents': ['plugins/unbound'],
+           'gui': ['metrics/unbound.py', 'wato/check_parameters/unbound.py'],
+           'lib': ['python3/cmk/base/cee/plugins/bakery/unbound.py']},
+ 'name': 'unbound',
+ 'title': 'Unbound',
+ 'version': '1.2.0-20240421',
+ 'version.min_required': '2.2.0b1',
+ 'version.packaged': '2.2.0p24',
+ 'version.usable_until': None}