# Copyright 2022 Offensive Security
# SPDX-license-identifier: GPL-3.0-only

import os

from kali_tweaks.utils import (
    run_as_root,
    say,
    write_file_as_root,
)


class SysctlSetting:
    """
    Sysctl is a bit tricky to work with, because the default values are not
    written anywhere, so after a value is changed, there is now way to undo
    it, except if the value was saved *before* changing it.

    With that in mind, here it's not really a problem, because kali-tweaks'
    scope is narrow, regarding sysctl settings. At the moment, we are in a
    situation where we only want to change settings that *do have* a default
    value defined in /usr/lib/sysctl.d, and that we override with a file in
    /etc/sysctl.d (note that the default values are usually installed by the
    package kali-defaults).

    Therefore, our workflow is to either add an override, or remove it, and
    then run "/lib/systemd/systemd-sysctl ..." afterwards. This command has
    no effect if there's no value defined for the setting, but as said above,
    there should always be a value, either the default, or the override, so
    we're good.

    If you find yourself in a situation where this is not the case, with a
    setting that has no default value in /usr/lib/sysctl.d, maybe you're
    doing it wrong (ie. you need to set a default value, and ship it with
    the package kali-defaults). But maybe you know what you're doing, and
    you will have to rework the code under here.
    """

    def __init__(self):
        pass

    def _read_setting(self, setting):
        """
        Read a kernel setting

        We're interested in the current value, we read it straight from /proc
        and we don't care about existing sysctl config file, if any.
        """
        with open(f"/proc/sys/{setting}") as f:
            content = f.read()
        if content[-1] == "\n":
            content = content[:-1]
        return content

    def _write_sysctl_setting(self, name, setting, value):
        """
        Write a sysctl setting to /etc/sysctl.d.
        """
        fn = f"/etc/sysctl.d/50-kali-tweaks-{name}.conf"
        if not os.path.exists("/etc/sysctl.d"):
            run_as_root("mkdir /etc/sysctl.d", log=False)
        print(f"> Writing changes to {fn}")
        write_file_as_root(fn, f"{setting} = {value}")

    def _remove_sysctl_setting(self, name):
        """
        Remove a sysctl setting from /etc/sysctl.d.
        """
        fn = f"/etc/sysctl.d/50-kali-tweaks-{name}.conf"
        print(f"> Removing {fn}")
        run_as_root(f"rm -f {fn}", log=False)

    def _apply_setting(self, setting):
        """
        Apply a kernel setting.

        This function assume that there's a value defined for the setting, in
        one of the locations that are supported, cf. sysctl.d(5) for details.
        If there's no value defined, the command has no effect.
        """
        cmd = f"/lib/systemd/systemd-sysctl --prefix {setting}"
        run_as_root(cmd, log=False)

    def load(self):
        """
        Load various sysctl settings.
        """
        dmesg_restrict = self._read_setting("kernel/dmesg_restrict")
        dmesg_restrict = dmesg_restrict != "0"
        port_start = self._read_setting("net/ipv4/ip_unprivileged_port_start")
        ports_restrict = port_start != "0"

        return {
            "dmesg-restrict": dmesg_restrict,
            "ports-restrict": ports_restrict,
        }

    def save(self, config):
        """
        Save changes.

        At the moment, all of those changes are relative to the system's
        default, as defined in /usr/lib/sysctl.d, meaning that either we
        override the default, or we remove the override. It also means
        that we make (unchecked) assumptions on the system's default.

        Sounds fragile? Yes it is. We rely on (external) testing to validate
        that it works, and to report if ever it doesn't work.
        """
        say("Updating kernel settings")

        if "dmesg-restrict" in config:
            dmesg_restrict = config["dmesg-restrict"]
            if dmesg_restrict is True:
                # Restore kernel's default
                self._write_sysctl_setting("dmesg", "kernel.dmesg_restrict", "1")
            else:
                # Restore Kali's defaults
                self._remove_sysctl_setting("dmesg")
            self._apply_setting("kernel.dmesg_restrict")

        if "ports-restrict" in config:
            ports_restrict = config["ports-restrict"]
            if ports_restrict is True:
                # Restore kernel's default
                self._write_sysctl_setting(
                    "unprivileged-ports", "net.ipv4.ip_unprivileged_port_start", "1024"
                )
            else:
                # Restore Kali's default
                self._remove_sysctl_setting("unprivileged-ports")
            self._apply_setting("net.ipv4.ip_unprivileged_port_start")
