Skip to content

Commit

Permalink
Merge branch 'master' into dev-banner-feature
Browse files Browse the repository at this point in the history
  • Loading branch information
fastiuk committed Oct 7, 2024
2 parents b0f57f2 + b7f26d4 commit b050b4e
Show file tree
Hide file tree
Showing 20 changed files with 420 additions and 45 deletions.
4 changes: 4 additions & 0 deletions data/templates/limits.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,8 @@
# ftp - chroot /ftp
# @student - maxlogins 4

{% if max_sessions and max_sessions | int > 0 -%}
* - maxsyslogins {{ max_sessions }}
{% endif -%}

# End of file
45 changes: 45 additions & 0 deletions host_modules/file_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""File stat handler"""

from host_modules import host_service
import subprocess

MOD_NAME = 'file'
EXIT_FAILURE = 1

import os

class FileService(host_service.HostModule):
"""
Dbus endpoint that executes the file command
"""
@host_service.method(host_service.bus_name(MOD_NAME), in_signature='s', out_signature='ia{ss}')
def get_file_stat(self, path):
if not path:
return EXIT_FAILURE, {'error': 'Dbus get_file_stat called with no path specified'}

try:
file_stat = os.stat(path)

# Get last modified time in nanoseconds since epoch
last_modified = int(file_stat.st_mtime * 1e9) # Convert seconds to nanoseconds

# Get permissions in octal format
permissions = oct(file_stat.st_mode)[-3:]

# Get file size in bytes
size = file_stat.st_size

# Get current umask
current_umask = os.umask(0)
os.umask(current_umask) # Reset umask to previous value

return 0, {
'path': path,
'last_modified': str(last_modified), # Converting to string to maintain consistency
'permissions': permissions,
'size': str(size), # Converting to string to maintain consistency
'umask': oct(current_umask)[-3:]
}

except Exception as e:
return EXIT_FAILURE, {'error': str(e)}
43 changes: 30 additions & 13 deletions scripts/caclmgrd
Original file line number Diff line number Diff line change
Expand Up @@ -581,11 +581,20 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['ip6tables', '-P', 'OUTPUT', 'ACCEPT'])
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['ip6tables', '-F'])
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['ip6tables', '-X'])
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['ip6tables', '-t', 'raw', '-F'])

# Add iptables/ip6tables commands to allow all traffic from localhost
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['iptables', '-A', 'INPUT', '-s', '127.0.0.1', '-i', 'lo', '-j', 'ACCEPT'])
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['ip6tables', '-A', 'INPUT', '-s', '::1', '-i', 'lo', '-j', 'ACCEPT'])


if self.bfdAllowed:
iptables_cmds += self.get_bfd_iptable_commands(namespace)

if self.VxlanAllowed:
fvs = swsscommon.FieldValuePairs([("src_ip", self.VxlanSrcIP)])
iptables_cmds += self.get_vxlan_port_iptable_commands(namespace, fvs)

# Add iptables commands to allow internal docker traffic
iptables_cmds += self.generate_allow_internal_docker_ip_traffic_commands(namespace)

Expand Down Expand Up @@ -635,6 +644,10 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['iptables', '-A', 'INPUT', '-p', 'tcp', '--dport', '179', '-j', 'ACCEPT'])
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['ip6tables', '-A', 'INPUT', '-p', 'tcp', '--dport', '179', '-j', 'ACCEPT'])

# Add ip6tables commands to disable connection tracking for icmpv6 traffic
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['ip6tables', '-t', 'raw', '-A', 'PREROUTING', '-p', 'ipv6-icmp', '-j', 'NOTRACK'])
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['ip6tables', '-t', 'raw', '-A', 'OUTPUT', '-p', 'ipv6-icmp', '-j', 'NOTRACK'])

# Get current ACL tables and rules from Config DB

self._tables_db_info = config_db_connector.get_table(self.ACL_TABLE)
Expand Down Expand Up @@ -813,12 +826,6 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):
self.log_info(" " + ' '.join(cmd))

self.run_commands(iptables_cmds)
if self.bfdAllowed:
self.allow_bfd_protocol(namespace)
if self.VxlanAllowed:
fvs = swsscommon.FieldValuePairs([("src_ip", self.VxlanSrcIP)])
self.allow_vxlan_port(namespace, fvs)


self.update_control_plane_nat_acls(namespace, service_to_source_ip_map, config_db_connector)

Expand Down Expand Up @@ -886,24 +893,29 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):
finally:
new_config_db_connector.close("CONFIG_DB")

def allow_bfd_protocol(self, namespace):
def get_bfd_iptable_commands(self, namespace):
iptables_cmds = []
# Add iptables/ip6tables commands to allow all BFD singlehop and multihop sessions
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['iptables', '-I', 'INPUT', '2', '-p', 'udp', '-m', 'multiport', '--dports', '3784,4784', '-j', 'ACCEPT'])
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] + ['ip6tables', '-I', 'INPUT', '2', '-p', 'udp', '-m', 'multiport', '--dports', '3784,4784', '-j', 'ACCEPT'])
self.run_commands(iptables_cmds)
return iptables_cmds

def allow_vxlan_port(self, namespace, data):
def allow_bfd_protocol(self, namespace):
iptables_cmds = self.get_bfd_iptable_commands(namespace)
if iptables_cmds:
self.run_commands(iptables_cmds)


def get_vxlan_port_iptable_commands(self, namespace, data):
iptables_cmds = []
for fv in data:
if (fv[0] == "src_ip"):
self.VxlanSrcIP = fv[1]
break

if not self.VxlanSrcIP:
self.log_info("Received vxlan tunnel configuration without source ip")
return False

iptables_cmds = []
return iptables_cmds

# Add iptables/ip6tables commands to allow VxLAN packets
ip_addr = ipaddress.ip_address(self.VxlanSrcIP)
Expand All @@ -914,10 +926,15 @@ class ControlPlaneAclManager(daemon_base.DaemonBase):
iptables_cmds.append(self.iptables_cmd_ns_prefix[namespace] +
['iptables', '-I', 'INPUT', '2', '-p', 'udp', '-d', self.VxlanSrcIP, '--dport', '4789', '-j', 'ACCEPT'])

return iptables_cmds

def allow_vxlan_port(self, namespace, data):
iptables_cmds = self.get_vxlan_port_iptable_commands(namespace, data)
if not iptables_cmds:
return False
self.run_commands(iptables_cmds)
self.log_info("Enabled vxlan port for source ip " + self.VxlanSrcIP)
self.VxlanAllowed = True
return True

def block_vxlan_port(self, namespace):
if not self.VxlanSrcIP:
Expand Down
108 changes: 88 additions & 20 deletions scripts/hostcfgd
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,18 @@ LINUX_DEFAULT_PASS_MAX_DAYS = 99999
LINUX_DEFAULT_PASS_WARN_AGE = 7

# Ssh min-max values
SSH_MIN_VALUES={"authentication_retries": 3, "login_timeout": 1, "ports": 1}
SSH_MAX_VALUES={"authentication_retries": 100, "login_timeout": 600, "ports": 65535}
SSH_CONFIG_NAMES={"authentication_retries": "MaxAuthTries" , "login_timeout": "LoginGraceTime"}
SSH_MIN_VALUES={"authentication_retries": 3, "login_timeout": 1, "ports": 1,
"inactivity_timeout": 0, "max_sessions": 0}
SSH_MAX_VALUES={"authentication_retries": 100, "login_timeout": 600,
"ports": 65535, "inactivity_timeout": 35000,
"max_sessions": 100}
SSH_CONFIG_NAMES={"authentication_retries": "MaxAuthTries",
"login_timeout": "LoginGraceTime", "ports": "Port",
"inactivity_timeout": "ClientAliveInterval"}

ACCOUNT_NAME = 0 # index of account name
AGE_DICT = { 'MAX_DAYS': {'REGEX_DAYS': r'^PASS_MAX_DAYS[ \t]*(?P<max_days>\d*)', 'DAYS': 'max_days', 'CHAGE_FLAG': '-M '},
'WARN_DAYS': {'REGEX_DAYS': r'^PASS_WARN_AGE[ \t]*(?P<warn_days>\d*)', 'DAYS': 'warn_days', 'CHAGE_FLAG': '-W '}
AGE_DICT = { 'MAX_DAYS': {'REGEX_DAYS': r'^PASS_MAX_DAYS[ \t]*(?P<max_days>-?\d*)', 'DAYS': 'max_days', 'CHAGE_FLAG': '-M '},
'WARN_DAYS': {'REGEX_DAYS': r'^PASS_WARN_AGE[ \t]*(?P<warn_days>-?\d*)', 'DAYS': 'warn_days', 'CHAGE_FLAG': '-W '}
}
PAM_LIMITS_CONF_TEMPLATE = "/usr/share/sonic/templates/pam_limits.j2"
LIMITS_CONF_TEMPLATE = "/usr/share/sonic/templates/limits.conf.j2"
Expand Down Expand Up @@ -612,8 +617,8 @@ class AaaCfg(object):
e_operations = [item for sublist in zip(e_list, operations) for item in sublist]
with open(filename+'.new', 'w') as f:
subprocess.call(["sed"] + e_operations + [filename], stdout=f)
subprocess.call(["mv", '-f', filename, filename+'.old'])
subprocess.call(['mv', '-f', filename+'.new', filename])
subprocess.call(["cp", '-f', filename, filename+'.old'])
subprocess.call(['cp', '-f', filename+'.new', filename])

self.check_file_not_empty(filename)

Expand Down Expand Up @@ -916,15 +921,11 @@ class PasswHardening(object):
if passw_policies:
if 'state' in passw_policies:
if passw_policies['state'] == 'enabled':
if 'expiration' in passw_policies:
if int(self.passw_policies['expiration']) != 0: # value '0' meaning age policy is disabled
# the logic is to modify the expiration time according the last updated modificatiion
#
curr_expiration = int(passw_policies['expiration'])

if 'expiration_warning' in passw_policies:
if int(self.passw_policies['expiration_warning']) != 0: # value '0' meaning age policy is disabled
curr_expiration_warning = int(passw_policies['expiration_warning'])
# Special values of expiration/expiration warning
# 0: meaning password will be expired/warning immediately.
# -1: meaning password expired/warning never.
curr_expiration = int(passw_policies.get('expiration', -1))
curr_expiration_warning = int(passw_policies.get('expiration_warning', -1))

if self.is_passwd_aging_expire_update(curr_expiration, 'MAX_DAYS'):
# Set aging policy for existing users
Expand Down Expand Up @@ -1105,9 +1106,15 @@ class SshServer(object):
syslog.syslog(syslog.LOG_ERR, "Ssh {} {} out of range".format(key, value))
elif key in SSH_CONFIG_NAMES:
# search replace configuration - if not in config file - append
if key == "inactivity_timeout":
# translate min to sec.
value = int(value) * 60
kv_str = "{} {}".format(SSH_CONFIG_NAMES[key], str(value)) # name +' '+ value format
modify_single_file_inplace(SSH_CONFG_TMP,['-E', "/^#?" + SSH_CONFIG_NAMES[key]+"/{h;s/.*/"+
kv_str + "/};${x;/^$/{s//" + kv_str + "/;H};x}"])
elif key in ['max_sessions']:
# Ignore, these parameters handled in other modules
continue
else:
syslog.syslog(syslog.LOG_ERR, "Failed to update sshd config file - wrong key {}".format(key))

Expand Down Expand Up @@ -1322,16 +1329,31 @@ class PamLimitsCfg(object):
self.config_db = config_db
self.hwsku = ""
self.type = ""
self.max_sessions = None

# Load config from ConfigDb and render config file/
def update_config_file(self):
device_metadata = self.config_db.get_table('DEVICE_METADATA')
if "localhost" not in device_metadata:
ssh_server_policies = {}
try:
ssh_server_policies = self.config_db.get_table('SSH_SERVER')
except KeyError:
"""Dont throw except in case we don`t have SSH_SERVER config."""
pass

if "localhost" not in device_metadata and "POLICIES" not in ssh_server_policies:
return

self.read_localhost_config(device_metadata["localhost"])
self.read_max_sessions_config(ssh_server_policies.get("POLICIES", None))
self.render_conf_file()

# Read max_sessions config
def read_max_sessions_config(self, ssh_server_policies):
if ssh_server_policies is not None:
max_sess_cfg = ssh_server_policies.get('max_sessions', 0)
self.max_sessions = max_sess_cfg if max_sess_cfg != 0 else None

# Read localhost config
def read_localhost_config(self, localhost):
if "hwsku" in localhost:
Expand All @@ -1348,7 +1370,6 @@ class PamLimitsCfg(object):
def render_conf_file(self):
env = jinja2.Environment(loader=jinja2.FileSystemLoader('/'), trim_blocks=True)
env.filters['sub'] = sub

try:
template_file = os.path.abspath(PAM_LIMITS_CONF_TEMPLATE)
template = env.get_template(template_file)
Expand All @@ -1362,7 +1383,8 @@ class PamLimitsCfg(object):
template = env.get_template(template_file)
limits_conf = template.render(
hwsku=self.hwsku,
type=self.type)
type=self.type,
max_sessions=self.max_sessions)
with open(LIMITS_CONF, 'w') as f:
f.write(limits_conf)
except Exception as e:
Expand Down Expand Up @@ -1695,6 +1717,38 @@ class FipsCfg(object):
loader.set_fips(image, self.enforce)


class SerialConsoleCfg:

def __init__(self):
self.cache = {}

def load(self, cli_sessions_conf):
self.cache = cli_sessions_conf or {}
syslog.syslog(syslog.LOG_INFO,
f'SerialConsoleCfg: Initial config: {self.cache}')

def update_serial_console_cfg(self, key, data):
'''
Apply config flow:
inactivity_timeout | set here AND in ssh_config flow | serial-config.service restarted.
max_sessions | set by PamLimitsCfg | serial-config.service DOESNT restarted.
sysrq_capabilities | set here | serial-config.service restarted.
'''

if self.cache.get(key, {}) != data:
''' Config changed, need to restart the serial-config.service '''
syslog.syslog(syslog.LOG_INFO, f'Set serial-config parameter {key} value: {data}')
try:
run_cmd(['sudo', 'service', 'serial-config', 'restart'],
True, True)
except Exception:
syslog.syslog(syslog.LOG_ERR, f'Failed to update {key} serial-config.service config')
return
self.cache.update({key: data})

return


class BannerCfg(object):
"""
Banner Config Daemon
Expand Down Expand Up @@ -1824,6 +1878,9 @@ class HostConfigDaemon:
# Initialize FipsCfg
self.fipscfg = FipsCfg(self.state_db_conn)

# Initialize SerialConsoleCfg
self.serialconscfg = SerialConsoleCfg()

# Initialize BannerCfg
self.bannermsgcfg = BannerCfg()

Expand All @@ -1849,6 +1906,7 @@ class HostConfigDaemon:
ntp_global = init_data.get(swsscommon.CFG_NTP_GLOBAL_TABLE_NAME)
ntp_servers = init_data.get(swsscommon.CFG_NTP_SERVER_TABLE_NAME)
ntp_keys = init_data.get(swsscommon.CFG_NTP_KEY_TABLE_NAME)
serial_console = init_data.get('SERIAL_CONSOLE', {})
banner_messages = init_data.get(swsscommon.CFG_BANNER_MESSAGE_TABLE_NAME)

self.aaacfg.load(aaa, tacacs_global, tacacs_server, radius_global, radius_server, ldap_global, ldap_server)
Expand All @@ -1858,13 +1916,15 @@ class HostConfigDaemon:
self.sshscfg.load(ssh_server)
self.devmetacfg.load(dev_meta)
self.mgmtifacecfg.load(mgmt_ifc, mgmt_vrf)

self.rsyslogcfg.load(syslog_cfg, syslog_srv)
self.dnscfg.load(dns)
self.fipscfg.load(fips_cfg)
self.ntpcfg.load(ntp_global, ntp_servers, ntp_keys)
self.serialconscfg.load(serial_console)
self.bannermsgcfg.load(banner_messages)

self.pamLimitsCfg.update_config_file()

# Update AAA with the hostname
self.aaacfg.hostname_update(self.devmetacfg.hostname)

Expand All @@ -1885,6 +1945,8 @@ class HostConfigDaemon:

def ssh_handler(self, key, op, data):
self.sshscfg.policies_update(key, data)
self.pamLimitsCfg.update_config_file()

syslog.syslog(syslog.LOG_INFO, 'SSH Update: key: {}, op: {}, data: {}'.format(key, op, data))

def tacacs_server_handler(self, key, op, data):
Expand Down Expand Up @@ -2010,6 +2072,10 @@ class HostConfigDaemon:
data = self.config_db.get_table("FIPS")
self.fipscfg.fips_handler(data)

def serial_console_config_handler(self, key, op, data):
syslog.syslog(syslog.LOG_INFO, 'SERIAL_CONSOLE table handler...')
self.serialconscfg.update_serial_console_cfg(key, data)

def banner_handler(self, key, op, data):
syslog.syslog(syslog.LOG_INFO, 'BANNER_MESSAGE table handler...')
self.bannermsgcfg.banner_message(key, data)
Expand Down Expand Up @@ -2043,6 +2109,8 @@ class HostConfigDaemon:
self.config_db.subscribe('LDAP_SERVER', make_callback(self.ldap_server_handler))
self.config_db.subscribe('PASSW_HARDENING', make_callback(self.passwh_handler))
self.config_db.subscribe('SSH_SERVER', make_callback(self.ssh_handler))
# Handle SERIAL_CONSOLE
self.config_db.subscribe('SERIAL_CONSOLE', make_callback(self.serial_console_config_handler))
# Handle IPTables configuration
self.config_db.subscribe('LOOPBACK_INTERFACE', make_callback(self.lpbk_handler))
# Handle updates to src intf changes in radius
Expand Down
5 changes: 3 additions & 2 deletions scripts/sonic-host-server
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import dbus.service
import dbus.mainloop.glib

from gi.repository import GObject
from host_modules import config_engine, gcu, host_service, showtech, systemd_service
from host_modules import config_engine, gcu, host_service, showtech, systemd_service, file_service


def register_dbus():
Expand All @@ -22,7 +22,8 @@ def register_dbus():
'gcu': gcu.GCU('gcu'),
'host_service': host_service.HostService('host_service'),
'showtech': showtech.Showtech('showtech'),
'systemd': systemd_service.SystemdService('systemd')
'systemd': systemd_service.SystemdService('systemd'),
'file_stat': file_service.FileService('file')
}
for mod_name, handler_class in mod_dict.items():
handlers[mod_name] = handler_class
Expand Down
Loading

0 comments on commit b050b4e

Please sign in to comment.