#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Make Node Configuration
# Copyright (C) 2012-2024 by Thomas Dreibholz
#
# 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 3 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, see <http://www.gnu.org/licenses/>.
#
# Contact: dreibh@simula.no

import sys

# NorNet
from NorNetConfiguration import *
from NorNetAPI           import *
from NorNetNodeSetup     import *
from SysSetupCommons     import *



# ###########################################################################
# #### Firewall Configuration                                            ####
# ###########################################################################


# ###### Get option for enabling a basic firewall ###########################
def getLocalNodeBasicFirewalling():
   try:
      return checkParameter('NorNet_LocalNode_BasicFirewalling', [ 'yes', 'no'])
   except:
      return 'yes'


# ###### Get DNAT string ####################################################
def getLocalSiteDNATOptionsString():
   try:
      return str(NorNet_Configuration['NorNet_LocalSite_DNAT_Options'])
   except:
      return ''


# ###### Get name of additional external interface ##########################
def getCentralSiteAdditionalExtInterface():
   try:
      return NorNet_Configuration['NorNet_CentralSite_AdditionalExtInterface']
   except:
      return ''


# ###### Get NAT range ######################################################
def getCentralSiteNATRangeString():
   try:
      return str(NorNet_Configuration['NorNet_CentralSite_NAT_Range'])
   except:
      return ''


# ###### Get blacklist ######################################################
def getCentralSiteBlacklistString():
   try:
      return str(NorNet_Configuration['NorNet_CentralSite_Blacklist'])
   except:
      return ''


# ###### Get Squid parent cache #############################################
def getSquidTransparentProxy():
   return checkParameter('NorNet_Squid_TransparentProxy', [ 'on', 'off'])


# ###### Write firewall configuration line to IPv4 and IPv6 files ###########
def _writeFWConfig(v4File, v6File, line):
   v4File.write(line)
   v6File.write(line)


# ###### Write firewall rules ###############################################
def makeFirewallConfiguration(fullSiteList, localSite, localNode):

   log('Making firewall configuration for ' + localSite['site_long_name'] + ' ...')
   if getLocalNodeBasicFirewalling() == 'no':
      # Nothing to be done!
      return

   localSiteIndex    = localSite['site_index']
   localProviderList = getNorNetProvidersForSite(localSite)

   # ====== Generate configuration files ====================================
   v4File = codecs.open('firewall4-config', 'w', 'utf-8')
   v6File = codecs.open('firewall6-config', 'w', 'utf-8')

   _writeFWConfig(v4File, v6File, "*filter\n")
   _writeFWConfig(v4File, v6File, ":INPUT ACCEPT [0:0]\n")

   # ------ Reject external RPC portmapper access ---------
   restrictedPortsUDP = [ '53', '111', '161' ]
   restrictedPortsTCP = [ '53', '111', '161', '3128:3129' ]

   for port in restrictedPortsUDP:
      for prefix in [ '127.0.0.1', str(NorNet_Configuration['NorNet_IPv4Prefix']), str(NorNet_Configuration['NorNet_IPv4TunnelPrefix']) ]:
         v4File.write("-A INPUT -s " + prefix + " -p udp -m udp --dport " + port + " -j ACCEPT\n")
      v4File.write("-A INPUT -p udp -m udp --dport " + port + " -j REJECT\n")
   for port in restrictedPortsTCP:
      for prefix in [ '127.0.0.1', str(NorNet_Configuration['NorNet_IPv4Prefix']), str(NorNet_Configuration['NorNet_IPv4TunnelPrefix']) ]:
         v4File.write("-A INPUT -s " + prefix + " -p tcp -m tcp --dport " + port + " -j ACCEPT\n")
      v4File.write("-A INPUT -p tcp -m tcp --dport " + port + " -j REJECT\n")

   for port in restrictedPortsUDP:
      for prefix in [ '::1', str(NorNet_Configuration['NorNet_IPv6Prefix']), str(NorNet_Configuration['NorNet_IPv6TunnelPrefix']) ]:
         v6File.write("-A INPUT -s " + prefix + " -p udp -m udp --dport " + port + " -j ACCEPT\n")
      v6File.write("-A INPUT -p udp -m udp --dport " + port + " -j REJECT\n")
   for port in restrictedPortsTCP:
      for prefix in [ '::1', str(NorNet_Configuration['NorNet_IPv6Prefix']), str(NorNet_Configuration['NorNet_IPv6TunnelPrefix']) ]:
         v6File.write("-A INPUT -s " + prefix + " -p tcp -m tcp --dport " + port + " -j ACCEPT\n")
      v6File.write("-A INPUT -p tcp -m tcp --dport " + port + " -j REJECT\n")
   # ------------------------------------------------------

   _writeFWConfig(v4File, v6File, ":FORWARD ACCEPT [0:0]\n")
   _writeFWConfig(v4File, v6File, ":OUTPUT ACCEPT [0:0]\n")
   _writeFWConfig(v4File, v6File, ":logreject - [0:0]\n")
   _writeFWConfig(v4File, v6File, "-A logreject -m limit --limit 2/min -j LOG --log-prefix \"Blacklist-Reject: \" --log-level 4\n")
   v4File.write("-A logreject -j REJECT --reject-with icmp-admin-prohibited\n")
   v6File.write("-A logreject -j REJECT --reject-with icmp6-adm-prohibited\n")


   # ====== Tunnelbox ====================================================
   if localNode['node_index'] == NorNet_NodeIndex_Tunnelbox:
      # ====== Get DNAT options ==========================================
      dnatOptions       = []
      dnatOptionsString = getLocalSiteDNATOptionsString()
      if ((dnatOptionsString != None) and (dnatOptionsString != '')):
         parameters = shlex.split(dnatOptionsString)
         for i in range(0, len(parameters)):
            try:
               options = unquote(parameters[i]).split()
               if len(options) != 4:
                  error('Bad configuration "' + parameters[i] + '" inside NorNet_LocalSite_DNAT_Options')
               interface = options[0]
               protocol  = options[1]
               if ((protocol != 'tcp') and
                  (protocol != 'udp') and
                  (protocol != 'sctp')):
                  error('Bad configuration "' + parameters[i] + '" inside NorNet_LocalSite_DNAT_Options: invalid protocol!')
               portRange = options[2]
               try:
                  destination = IPv4Address(options[3])
               except:
                  error('Bad configuration "' + parameters[i] + '" inside NorNet_LocalSite_DNAT_Options: destination is not an IPv4 address!')
               dnatOptions.append([ interface, protocol, portRange, destination])
            except Exception as e:
               error('Bad configuration "' + dnatOptionsString + '" for NorNet_LocalSite_DNAT_Options: ' + str(e))


      # ====== Get external interfaces ======================================
      externalInterfaces = []
      if getCentralSiteAdditionalExtInterface() != '':
         externalInterfaces.add(getCentralSiteAdditionalExtInterface())
      providerList = getNorNetProvidersForSite(localSite)
      for onlyDefault in [ True, False ]:
         for providerIndex in providerList:
            if ( ((onlyDefault == True)  and (providerIndex == localSite['site_default_provider_index'])) or \
               ((onlyDefault == False) and (providerIndex != localSite['site_default_provider_index'])) ):
               provider = providerList[providerIndex]
               externalInterfaces.append(provider['provider_tunnelbox_interface'])


      if localSiteIndex == NorNet_SiteIndex_Central:
         # ====== Get blacklist =============================================
         natBlacklist       = []
         natBlacklistString = getCentralSiteBlacklistString()
         if ((natBlacklistString != None) and (natBlacklistString != '')):
            parameters = shlex.split(natBlacklistString)
            for i in range(0, len(parameters)):
               try:
                  natBlacklist.append(ip_interface(parameters[i]))
               except Exception as e:
                  error('Bad configuration "' + natBlacklistString + '" for NorNet_CentralSite_Blacklist: ' + str(e))

         # ====== Write FORWARD chain configuration =========================
         for interfaceName in externalInterfaces:
            for blacklistNetwork in natBlacklist:
               if blacklistNetwork.version == 4:
                  v4File.write('-A FORWARD -s ' + str(blacklistNetwork) + ' -o ' + interfaceName + ' -j logreject\n')
               else:
                  v6File.write('-A FORWARD -s ' + str(blacklistNetwork) + ' -o ' + interfaceName + ' -j logreject\n')


      if localSiteIndex == NorNet_SiteIndex_Central:
         # ====== Get NAT range =============================================
         sourceNatRangeString = getCentralSiteNATRangeString()
         sourceNatRange       = None
         if ((sourceNatRangeString != None) and (sourceNatRangeString != '')):
            parameters = shlex.split(sourceNatRangeString)
            try:
               if len(parameters) == 1:
                  a1 = IPv4Address(unquote(parameters[0]))
                  sourceNatRange = [ a1 ]
               elif len(parameters) > 1:
                  a1 = IPv4Address(unquote(parameters[0]))
                  a2 = IPv4Address(unquote(parameters[1]))
                  sourceNatRange = [ a1, a2 ]
            except Exception as e:
               error('Bad configuration "' + sourceNatRangeString + '" for NorNet_CentralSite_NAT_Range: ' + str(e))

         # ====== Get target (MASQUERADE or SNAT) ===========================
         if sourceNatRange != None:
            if len(sourceNatRange) == 1:
               natTraget = 'SNAT --random --to-source ' + str(sourceNatRange[0])
            else:
               natTraget = 'SNAT --random --to-source ' + str(sourceNatRange[0]) + '-' + str(sourceNatRange[1])
         else:
            natTraget = 'MASQUERADE --random'
      else:
         natTraget = None

      # ====== Write IPv4 NAT configuration =================================
      v4File.write("COMMIT\n")
      v4File.write("*nat\n")
      v4File.write(":PREROUTING ACCEPT [0:0]\n")
      v4File.write(":INPUT ACCEPT [0:0]\n")
      v4File.write(":OUTPUT ACCEPT [0:0]\n")
      v4File.write(":POSTROUTING ACCEPT [0:0]\n")
      if natTraget != None:
         for interfaceName in externalInterfaces:
            v4File.write('-A POSTROUTING -s ' + str(NorNet_Configuration['NorNet_IPv4Prefix'])       + ' -o ' + interfaceName + ' -j ' + natTraget + "\n")
            v4File.write('-A POSTROUTING -s ' + str(NorNet_Configuration['NorNet_IPv4TunnelPrefix']) + ' -o ' + interfaceName + ' -j ' + natTraget + "\n")

      if getSquidTransparentProxy() == 'on':
         v4File.write('-A PREROUTING -i "' + str(NorNet_Configuration['NorNet_LocalNode_NorNetInterface']) + '" -p tcp' + \
                     ' ! -d ' + str(makeNorNetIP(0, 0, 0, 4)) + ' --dport 80 -j REDIRECT --to-port 3129' + "\n")

      if len(dnatOptions) > 0:
         for dnatOption in dnatOptions:
            v4File.write('-A PREROUTING -i "' + dnatOption[0] + '"' + \
                        ' -p "' + dnatOption[1] + '"' + \
                        ' --dport "' + dnatOption[2] + '"' + \
                        ' -j DNAT --to ' + str(dnatOption[3]) + "\n")

   _writeFWConfig(v4File, v6File, "COMMIT\n")

   v6File.close()
   v4File.close()



# ###### Generate NTP configuration #########################################
def makeNTPConfiguration(fullSiteList, localSite, localNode):
   outputFile = codecs.open('ntp-config', 'w', 'utf-8')
   writeAutoConfigInformation(outputFile)

   customNTPServerList = False
   ntpServerList       = []
   ntpServerNameList   = []
   try:
      ntpServerNameList = NorNet_Configuration['NorNet_LocalNode_NTPServers'].split(' ')
   except:
      pass

   if len(ntpServerNameList) > 0:
      for ntpServerName in ntpServerNameList:
         success = False
         for type in [ socket.AF_INET6, socket.AF_INET ]:
            try:
               result = socket.getaddrinfo (ntpServerName, 123, type, socket.SOCK_DGRAM)
               for resultItem in result:
                  ntpServerList.append(str(ip_address(resultItem[4][0])))
                  success             = True
                  customNTPServerList = True
            except:
               continue
         if success == False:
            error('Unable to resolve NTP server name: ' + ntpServerName)
   else:
      if localSite != None:
         for i in range(0, NorNet_MaxNTPServers - 1):
            ntpServer = ip_address(getTagValue(localSite['site_tags'], 'nornet_site_ntp' + str(1 + i), '0.0.0.0'))
            if ntpServer != IPv4Address('0.0.0.0'):
               ntpServerList.append(str(ntpServer))

   ntpServerList = sorted(set(ntpServerList))

   outputFile.write('# ====== Drift File ======\n')
   outputFile.write('driftfile /var/lib/ntp/ntp.drift\n\n')

   outputFile.write('# ====== Statistics ======\n')
   outputFile.write('statsdir /var/log/ntpstats/\n')
   outputFile.write('filegen loopstats file loopstats type day enable\n')
   outputFile.write('filegen peerstats file peerstats type day enable\n')
   outputFile.write('filegen clockstats file clockstats type day enable\n\n')

   outputFile.write('# ====== Generic Access Restrictions ======\n')
   outputFile.write('restrict default ignore\n')
   if localNode['node_index'] == NorNet_NodeIndex_Tunnelbox:
      for version in [ 4, 6 ]:
         fullNorNetNetwork = makeNorNetIP(0, 0, 0, version)
         outputFile.write('restrict ' + str(fullNorNetNetwork.ip) + ' mask ' + str(fullNorNetNetwork.netmask) + ' nomodify\n')
   outputFile.write('restrict 127.0.0.1\n')
   outputFile.write('restrict ::1\n')

   outputFile.write('\n# ====== Fudge Clock ======\n')
   outputFile.write('server 127.127.1.0\n')
   outputFile.write('fudge 127.127.1.0 stratum 10\n\n')


   # ====== Write tunnelbox NTP server configuration ========================
   if localNode['node_index'] == NorNet_NodeIndex_Tunnelbox:
      if ((localSite == None) or (localSite['site_index'] != NorNet_SiteIndex_Central)):
         if fullSiteList != None:
            outputFile.write('# ====== NorNet Central Site NTP ======\n')
            centralSite  = fullSiteList[NorNet_SiteIndex_Central]
            providerList = getNorNetProvidersForSite(centralSite)
            for providerIndex in providerList:
               provider = providerList[providerIndex]
               if providerIndex == centralSite['site_default_provider_index']:
                  for version in [ 6 ]:
                     centralSiteTB = makeNorNetIP(providerIndex, NorNet_SiteIndex_Central, NorNet_NodeIndex_Tunnelbox, version)
                     outputFile.write('server ' + str(centralSiteTB.ip) + '   # CENTRAL SITE\n')
                     outputFile.write('restrict ' + str(centralSiteTB.ip) + '\n')
            outputFile.write('\n')

      outputFile.write('# ====== NorNet Peers ======\n')
      for remoteSiteIndex in fullSiteList:
         if ( ((localSite == None) or (remoteSiteIndex != localSite['site_index'])) and
              (remoteSiteIndex != NorNet_SiteIndex_Central) ):
            for version in [ 6 ]:
               remoteSite = fullSiteList[remoteSiteIndex]
               peerTB = makeNorNetIP(remoteSite['site_default_provider_index'], remoteSiteIndex, NorNet_NodeIndex_Tunnelbox, version)
               outputFile.write('peer ' + str(peerTB.ip) + '   # ' + remoteSite['site_long_name'] + '\n')
               outputFile.write('restrict ' + str(peerTB.ip) + '\n')
      outputFile.write('\n')

      outputFile.write('# ====== External NTP Servers ======\n')
      for ntpServer in ntpServerList:
         outputFile.write('server ' + str(ntpServer) + '\n')
         outputFile.write('restrict ' + str(ntpServer) + '\n')

   # ====== Write non-tunnelbox NTP server configuration ===================#
   else:
      outputFile.write('# ====== Local NTP Server ======\n')
      if customNTPServerList == False:
         ntpServer = makeNorNetIP(localSite['site_default_provider_index'],
                                 localSite['site_index'], NorNet_NodeIndex_Tunnelbox, 6)
         outputFile.write('server ' + str(ntpServer.ip) + '\n')
         outputFile.write('restrict ' + str(ntpServer.ip) + '\n')
      else:
         for ntpServer in ntpServerList:
            outputFile.write('server ' + str(ntpServer) + '\n')
            outputFile.write('restrict ' + str(ntpServer) + '\n')

   outputFile.close()


# ###### Generate SNMP configuration ########################################
def makeSNMPConfiguration(fullSiteList, fullUserList, localSite, name, description):
   outputFile = codecs.open('snmpd-config', 'w', 'utf-8')
   writeAutoConfigInformation(outputFile)


   outputFile.write('# ====== Agent ======\n')
   outputFile.write('agentAddress udp:161,udp6:[::1]:161\n\n')


   outputFile.write('# ====== System Information ======\n')
   country      = getTagValue(localSite['site_tags'], 'nornet_site_country', '???')
   province     = getTagValue(localSite['site_tags'], 'nornet_site_province', None)
   city         = getTagValue(localSite['site_tags'], 'nornet_site_city',    '???')
   outputFile.write('sysName     ' + name + '.' + localSite['site_domain'] + '\n')
   outputFile.write('sysDescr    ' + localSite['site_long_name'] + ' ' + description + '\n')
   outputFile.write('sysLocation ' + city)
   if province !=  None:
      outputFile.write(', ' + province)
   outputFile.write('/' + country + '\n')
   techUsers = fetchUsersOfNorNetSite(fullUserList, localSite, 'tech')
   if techUsers != None:
      outputFile.write('sysContact  ' +
                       techUsers[0]['user_title'] + ' ' +
                       techUsers[0]['user_first_name'] + ' ' +
                       techUsers[0]['user_last_name'] + ' ' +
                       '<' + techUsers[0]['user_email'] + '>\n')
   outputFile.write('sysServices 72\n\n')


   outputFile.write('# ====== Access Control ======\n')
   outputFile.write('rocommunity public 127.0.0.1\n')
   outputFile.write('rocommunity public ' + str(makeNorNetIP(0, 0, 0, 4)) + '\n')
   outputFile.write('rocommunity6 public ::1\n')
   outputFile.write('rocommunity6 public ' + str(makeNorNetIP(0, 0, 0, 6)) + '\n\n')

   outputFile.write('# ====== Active Monitoring ======\n')
   outputFile.write('trapcommunity           public\n')
   # !!! FIXME: The provider index may be wrong! It must be default index of Central Site!
   outputFile.write('trapsink                ' + str(makeNorNetIP(localSite['site_default_provider_index'],
                                                                  NorNet_SiteIndex_Monitor,
                                                                  NorNet_NodeIndex_Monitor, 4).ip) + '\n')
   outputFile.write('iquerySecName           internalUser\n')
   outputFile.write('rouser                  internalUser\n')
   outputFile.write('# defaultMonitors         yes\n')
   outputFile.write('# linkUpDownNotifications yes\n\n')

   outputFile.write('# ====== Disk Monitoring (UCD-SNMP-MIB::dskTable) ======\n')
   outputFile.write('includeAllDisks 10%\n\n')

   outputFile.write('# ====== Load Monitoring (UCD-SNMP-MIB::laTable) ======\n')
   outputFile.write('load 12 10 5\n')

   outputFile.close()



# ###### Main program #######################################################
# ====== Handle arguments ===================================================
if len(sys.argv) < 4:
   error('Usage: ' + sys.argv[0] + ' FQDN Interface Debian|Fedora|Netplan|FreeBSD')
fqdn          = sys.argv[1]
interfaceName = sys.argv[2]
distribution  = sys.argv[3]


# ====== Get configuration ==================================================
try:
   loginToPLC()
except Exception as e:
   sys.stderr.write('ERROR: ' + str(e) + '!\n')
   sys.exit(1)
fullSiteList = fetchNorNetSiteList(False)

localNode = fetchNorNetNode(fqdn)
if localNode == None:
   print(('Node ' + fqdn + ' is not in the database => just using INDEX ' + \
         str(getLocalNodeIndex()) + ' from configuration file.'))
   if getLocalNodeIndex() == None:
      error('NorNet_LocalNode_Index has not been set in configuration file!')
   if getLocalNodeNorNetInterface() == None:
      error('NorNet_LocalNode_NorNetInterface has not been set in configuration file!')
   domain = getDomainFromFQDN(fqdn)
   localSite = getNorNetSiteOfDomain(fullSiteList, domain)
   if localSite == None:
      error('Unable to find site for domain ' + domain + ' in database!')
   localNode = {
      'node_id'               : -1,
      'node_site_id'          : localSite['site_id'],
      'node_index'            : getLocalNodeIndex(),
      'node_name'             : getHostnameFromFQDN(fqdn),
      'node_model'            : 'Amiga 6000',
      'node_nornet_interface' : getLocalNodeNorNetInterface()
  }

localSite = getNorNetSiteOfNode(fullSiteList, localNode)
if localSite == None:
   error('Unable to find site of node ' + localNode['node_name'] + ' in database!')
providerList = getNorNetProvidersForSite(localSite)
fullUserList = fetchNorNetUserList()


writeHostname('hostname-config', localNode['node_name'], localSite['site_domain'])
writeHosts('hosts-config', localNode['node_name'], localSite['site_domain'])
writeSysctlConfiguration('sysctl-config', getLocalNodeNorNetInterface())

writeInterfaceConfiguration('', distribution, interfaceName,
                            NorNet_Configuration['NorNet_LocalNode_ControlBox'],
                            localNode['node_name'], localSite['site_domain'], localNode['node_index'], localSite['site_index'],
                            providerList, localSite['site_default_provider_index'],
                            matchInterface = getLocalNodeNorNetInterfaceMatch())
writeProxyConfiguration('', localSite['site_domain'], distribution,
                        False)   # Use tunnelbox's proxy!

makeFirewallConfiguration(fullSiteList, localSite, localNode)
makeNTPConfiguration(fullSiteList, localSite, localNode)
makeSNMPConfiguration(fullSiteList, fullUserList, localSite, localNode['node_name'], 'Node')

# Create AutoFS configuration only if we are not on the file server itself!
# (otherwise: just add empty AutoFS configuration)
weAreTheFileServer = ((localSite['site_index'] == NorNet_SiteIndex_FileSrv) and
                      (localNode['node_index'] == NorNet_NodeIndex_FileSrv))
makeAutoFSConfiguration(weAreTheFileServer, localSite['site_index'], localNode['node_index'], True)
