diff --git a/agent/collectors/windows_arm64.go b/agent/collectors/windows_arm64.go index a71bd68e7..155f1a4e2 100644 --- a/agent/collectors/windows_arm64.go +++ b/agent/collectors/windows_arm64.go @@ -4,215 +4,320 @@ package collectors import ( + "encoding/json" + "encoding/xml" + "fmt" + "log" "os" - "os/exec" - "path/filepath" + "os/signal" + "strconv" "strings" - "time" + "syscall" + "unsafe" + "github.com/threatwinds/go-sdk/plugins" + "github.com/threatwinds/validations" "github.com/utmstack/UTMStack/agent/config" "github.com/utmstack/UTMStack/agent/logservice" - - "github.com/threatwinds/validations" "github.com/utmstack/UTMStack/agent/utils" + "golang.org/x/sys/windows" ) -type Windows struct{} +type Event struct { + XMLName xml.Name `xml:"Event"` + System SystemData `xml:"System"` + EventData []*EventData `xml:"EventData>Data"` +} -func getCollectorsInstances() []Collector { - var collectors []Collector - collectors = append(collectors, Windows{}) - return collectors +type EventData struct { + Key string `xml:"Name,attr"` + Value string `xml:",chardata"` +} + +type ProviderData struct { + ProviderName string `xml:"Name,attr"` + ProviderGUID string `xml:"Guid,attr"` } -const PowerShellScript = ` -<# -.SYNOPSIS - Collects Windows Application, System, and Security logs from the last 5 minutes, then prints them to the console in a compact, single-line JSON format, - emulating the field structure that Winlogbeat typically produces. - -.DESCRIPTION - 1. Retrieves the last 5 minutes of Windows logs (Application, System, Security) using FilterHashtable (no post-fetch filtering). - 2. Maps event properties to a schema similar to Winlogbeat's, including: - - @timestamp - - message - - event.code - - event.provider - - event.kind - - winlog fields (e.g. record_id, channel, activity_id, etc.) - 3. Prints each log record as a single-line JSON object with no indentation/extra spacing. - 4. If no logs are found, the script produces no output at all. -#> - -# Suppress any runtime errors that would clutter the console. -$ErrorActionPreference = 'SilentlyContinue' - -# Calculate the start time for filtering -$startTime = (Get-Date).AddSeconds(-30) - -# Retrieve logs with filter hashtable -$applicationLogs = Get-WinEvent -FilterHashtable @{ LogName='Application'; StartTime=$startTime } -$systemLogs = Get-WinEvent -FilterHashtable @{ LogName='System'; StartTime=$startTime } -$securityLogs = Get-WinEvent -FilterHashtable @{ LogName='Security'; StartTime=$startTime } - -# Safeguard against null results -if (-not $applicationLogs) { $applicationLogs = @() } -if (-not $systemLogs) { $systemLogs = @() } -if (-not $securityLogs) { $securityLogs = @() } - -# Combine them -$recentLogs = $applicationLogs + $systemLogs + $securityLogs - -# If no logs are found, produce no output at all -if (-not $recentLogs) { - return -} - -# Function to convert the raw Properties array to a dictionary-like object under winlog.event_data -function Convert-PropertiesToEventData { - param([Object[]] $Properties) - - # If nothing is there, return an empty hashtable - if (-not $Properties) { return @{} } - - # Winlogbeat places custom fields under winlog.event_data. - # Typically, it tries to parse known keys, but we'll do a simple best-effort approach: - # We'll create paramN = pairs for each array index. - $eventData = [ordered]@{} - - for ($i = 0; $i -lt $Properties.Count; $i++) { - $value = $Properties[$i].Value - - # If the property is itself an object with nested fields, we can flatten or store as-is. - # We'll store as-is for clarity. - # We'll name them param1, param2, param3,... unless you'd like more specific field logic. - $paramName = "param$($i+1)" - - $eventData[$paramName] = $value - } - - return $eventData -} - -# Transform each event into a structure emulating Winlogbeat -foreach ($rawEvent in $recentLogs) { - # Convert TimeCreated to a universal ISO8601 string - $timestamp = $rawEvent.TimeCreated.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') - - # Build the top-level document - $doc = [ordered]@{ - # Matches Winlogbeat's typical top-level timestamp field - '@timestamp' = $timestamp - - # The main message content from the event - 'message' = $rawEvent.Message - - # "event" block: minimal example - 'event' = [ordered]@{ - 'code' = $rawEvent.Id # event_id in Winlogbeat is typically a string or numeric - 'provider' = $rawEvent.ProviderName - 'kind' = 'event' - 'created' = $timestamp # or you could omit if desired - } - - # "winlog" block: tries to mirror Winlogbeat's structure for Windows - 'winlog' = [ordered]@{ - 'record_id' = $rawEvent.RecordId - 'computer_name' = $rawEvent.MachineName - 'channel' = $rawEvent.LogName - 'provider_name' = $rawEvent.ProviderName - 'provider_guid' = $rawEvent.ProviderId - 'process' = [ordered]@{ - 'pid' = $rawEvent.ProcessId - 'thread' = @{ - 'id' = $rawEvent.ThreadId - } - } - 'event_id' = $rawEvent.Id - 'version' = $rawEvent.Version - 'activity_id' = $rawEvent.ActivityId - 'related_activity_id'= $rawEvent.RelatedActivityId - 'task' = $rawEvent.TaskDisplayName - 'opcode' = $rawEvent.OpcodeDisplayName - 'keywords' = $rawEvent.KeywordsDisplayNames - 'time_created' = $timestamp - # Convert "Properties" into a dictionary for event_data - 'event_data' = Convert-PropertiesToEventData $rawEvent.Properties - } - } - - # Convert our object to JSON (with no extra formatting). - $json = $doc | ConvertTo-Json -Depth 20 - - # Remove all newlines and indentation for a single-line representation - $compactJson = $json -replace '(\r?\n\s*)+', '' - - # Output the line - Write-Output $compactJson -} -` +type TimeCreatedData struct { + SystemTime string `xml:"SystemTime,attr"` +} -func (w Windows) Install() error { - return nil +type CorrelationData struct { + ActivityID string `xml:"ActivityID,attr"` } -func (w Windows) SendSystemLogs() { - path := utils.GetMyPath() - collectorPath := filepath.Join(path, "collector.ps1") +type ExecutionData struct { + ProcessID int `xml:"ProcessID,attr"` + ThreadID int `xml:"ThreadID,attr"` +} + +type SecurityData struct{} + +type SystemData struct { + Provider ProviderData `xml:"Provider"` + EventID int `xml:"EventID"` + Version int `xml:"Version"` + Level int `xml:"Level"` + Task int `xml:"Task"` + Opcode int `xml:"Opcode"` + Keywords string `xml:"Keywords"` + TimeCreated TimeCreatedData `xml:"TimeCreated"` + EventRecordID int64 `xml:"EventRecordID"` + Correlation CorrelationData `xml:"Correlation"` + Execution ExecutionData `xml:"Execution"` + Channel string `xml:"Channel"` + Computer string `xml:"Computer"` + Security SecurityData `xml:"Security"` +} + +type EventSubscription struct { + Channel string + Query string + Errors chan error + Callback func(event *Event) + winAPIHandle windows.Handle +} - err := os.WriteFile(collectorPath, []byte(PowerShellScript), 0644) +const ( + EvtSubscribeToFutureEvents = 1 + evtSubscribeActionError = 0 + evtSubscribeActionDeliver = 1 + evtRenderEventXML = 1 +) + +var ( + modwevtapi = windows.NewLazySystemDLL("wevtapi.dll") + procEvtSubscribe = modwevtapi.NewProc("EvtSubscribe") + procEvtRender = modwevtapi.NewProc("EvtRender") + procEvtClose = modwevtapi.NewProc("EvtClose") +) + +func (evtSub *EventSubscription) Create() error { + if evtSub.winAPIHandle != 0 { + return fmt.Errorf("windows_events: subscription has already been created") + } + + winChannel, err := windows.UTF16PtrFromString(evtSub.Channel) if err != nil { - _ = utils.Logger.ErrorF("error writing powershell script: %v", err) - return + return fmt.Errorf("windows_events: invalid channel name: %s", err) } - cmd := exec.Command("Powershell.exe", "-Command", `"Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser -Force"`) - err = cmd.Run() + winQuery, err := windows.UTF16PtrFromString(evtSub.Query) if err != nil { - _ = utils.Logger.ErrorF("error setting powershell execution policy: %v", err) - return + return fmt.Errorf("windows_events: invalid query: %s", err) } - for { - select { - case <-time.After(30 * time.Second): - go func() { - cmd := exec.Command("Powershell.exe", "-File", collectorPath) + callback := syscall.NewCallback(evtSub.winAPICallback) - output, err := cmd.Output() - if err != nil { - _ = utils.Logger.ErrorF("error executing powershell script: %v", err) - return - } + log.Printf("Debug - Subscribing to channel: %s", evtSub.Channel) - utils.Logger.LogF(100, "output: %s", string(output)) + handle, _, err := procEvtSubscribe.Call( + 0, + 0, + uintptr(unsafe.Pointer(winChannel)), + uintptr(unsafe.Pointer(winQuery)), + 0, + 0, + callback, + uintptr(EvtSubscribeToFutureEvents), + ) - logLines := strings.Split(string(output), "\n") + if handle == 0 { + return fmt.Errorf("windows_events: failed to subscribe to events: %v", err) + } - validatedLogs := make([]string, 0, len(logLines)) + evtSub.winAPIHandle = windows.Handle(handle) + return nil +} - for _, logLine := range logLines { - validatedLog, _, err := validations.ValidateString(logLine, false) - if err != nil { - utils.Logger.LogF(100, "error validating log: %s: %v", logLine, err) - continue - } +func (evtSub *EventSubscription) Close() error { + if evtSub.winAPIHandle == 0 { + return fmt.Errorf("windows_events: no active subscription to close") + } + ret, _, err := procEvtClose.Call(uintptr(evtSub.winAPIHandle)) + if ret == 0 { + return fmt.Errorf("windows_events: error closing handle: %s", err) + } + evtSub.winAPIHandle = 0 + return nil +} - validatedLogs = append(validatedLogs, validatedLog) +func (evtSub *EventSubscription) winAPICallback(action, userContext, event uintptr) uintptr { + switch action { + case evtSubscribeActionError: + evtSub.Errors <- fmt.Errorf("windows_events: error in callback, code: %x", uint16(event)) + case evtSubscribeActionDeliver: + bufferSize := uint32(4096) + for { + renderSpace := make([]uint16, bufferSize/2) + bufferUsed := uint32(0) + propertyCount := uint32(0) + ret, _, err := procEvtRender.Call( + 0, + event, + evtRenderEventXML, + uintptr(bufferSize), + uintptr(unsafe.Pointer(&renderSpace[0])), + uintptr(unsafe.Pointer(&bufferUsed)), + uintptr(unsafe.Pointer(&propertyCount)), + ) + if ret == 0 { + if err == windows.ERROR_INSUFFICIENT_BUFFER { + bufferSize *= 2 + continue } + evtSub.Errors <- fmt.Errorf("windows_events: failed to render event: %w", err) + return 0 + } + xmlStr := windows.UTF16ToString(renderSpace) + xmlStr = cleanXML(xmlStr) + + dataParsed := new(Event) + if err := xml.Unmarshal([]byte(xmlStr), dataParsed); err != nil { + evtSub.Errors <- fmt.Errorf("windows_events: failed to parse XML: %s", err) + } else { + evtSub.Callback(dataParsed) + } + break + } + default: + evtSub.Errors <- fmt.Errorf("windows_events: unsupported action in callback: %x", uint16(action)) + } + return 0 +} - logservice.LogQueue <- logservice.LogPipe{ - Src: string(config.DataTypeWindowsAgent), - Logs: validatedLogs, - } - }() +func cleanXML(xmlStr string) string { + xmlStr = strings.TrimSpace(xmlStr) + if idx := strings.Index(xmlStr, " 0 { + xmlStr = xmlStr[idx:] + } + xmlStr = strings.Map(func(r rune) rune { + if r < 32 && r != '\n' && r != '\r' && r != '\t' { + return -1 } + return r + }, xmlStr) + return xmlStr +} + +type Windows struct{} + +func getCollectorsInstances() []Collector { + var collectors []Collector + collectors = append(collectors, Windows{}) + return collectors +} + +func (w Windows) SendSystemLogs() { + errorsChan := make(chan error, 10) + + callback := func(event *Event) { + eventJSON, err := convertEventToJSON(event) + if err != nil { + utils.Logger.ErrorF("error converting event to JSON: %v", err) + return + } + host, err := os.Hostname() + if err != nil { + utils.Logger.ErrorF("error getting hostname: %v", err) + host = "unknown" + } + validatedLog, _, err := validations.ValidateString(eventJSON, false) + if err != nil { + utils.Logger.LogF(100, "error validating log: %s: %v", eventJSON, err) + return + } + logservice.LogQueue <- &plugins.Log{ + DataType: string(config.DataTypeWindowsAgent), + DataSource: host, + Raw: validatedLog, + } + } + + channels := []string{"Security", "Application", "System"} + var subscriptions []*EventSubscription + + for _, channel := range channels { + sub := &EventSubscription{ + Channel: channel, + Query: "*", + Errors: errorsChan, + Callback: callback, + } + if err := sub.Create(); err != nil { + utils.Logger.ErrorF("Error subscribing to channel %s: %s", channel, err) + continue + } + subscriptions = append(subscriptions, sub) + utils.Logger.LogF(100, "Subscribed to channel: %s", channel) } + + go func() { + for err := range errorsChan { + utils.Logger.ErrorF("Subscription error: %s", err) + } + }() + + exitChan := make(chan os.Signal, 1) + signal.Notify(exitChan, os.Interrupt) + <-exitChan + utils.Logger.LogF(100, "Interrupt received, closing subscriptions...") + for _, sub := range subscriptions { + if err := sub.Close(); err != nil { + utils.Logger.ErrorF("Error closing subscription for %s: %v", sub.Channel, err) + } + } + utils.Logger.LogF(100, "Agent finished successfully.") +} + +func convertEventToJSON(event *Event) (string, error) { + eventMap := map[string]interface{}{ + "timestamp": event.System.TimeCreated.SystemTime, + "providerName": event.System.Provider.ProviderName, + "providerGuid": event.System.Provider.ProviderGUID, + "eventCode": event.System.EventID, + "version": event.System.Version, + "level": event.System.Level, + "task": event.System.Task, + "opcode": event.System.Opcode, + "keywords": event.System.Keywords, + "timeCreated": event.System.TimeCreated.SystemTime, + "recordId": event.System.EventRecordID, + "correlation": event.System.Correlation, + "execution": event.System.Execution, + "channel": event.System.Channel, + "computer": event.System.Computer, + "data": make(map[string]interface{}), + } + + dataMap := eventMap["data"].(map[string]interface{}) + for _, data := range event.EventData { + if strings.HasPrefix(data.Value, "0x") { + if val, err := strconv.ParseInt(data.Value[2:], 16, 64); err == nil { + dataMap[data.Key] = val + continue + } + } + if data.Key != "" { + value := strings.TrimSpace(data.Value) + if value != "" { + dataMap[data.Key] = value + } + } + } + + jsonBytes, err := json.Marshal(eventMap) + if err != nil { + return "", err + } + return string(jsonBytes), nil +} + +func (w Windows) Install() error { + return nil } func (w Windows) Uninstall() error { - path := utils.GetMyPath() - collectorPath := filepath.Join(path, "collector.ps1") - err := os.Remove(collectorPath) - return err + return nil } diff --git a/installer/cloud.go b/installer/cloud.go index 5cb8548a2..46e1f97e4 100644 --- a/installer/cloud.go +++ b/installer/cloud.go @@ -9,7 +9,8 @@ import ( "github.com/utmstack/UTMStack/installer/utils" ) -func Cloud(c *types.Config, update bool) error { +func Cloud(c *types.Config, update bool, distro string) error { + if err := utils.CheckCPU(2); err != nil { return err } @@ -43,7 +44,7 @@ func Cloud(c *types.Config, update bool) error { if utils.GetLock(2, stack.LocksDir) { fmt.Println("Preparing system to run UTMStack") - if err := PrepareSystem(); err != nil { + if err := PrepareSystem(distro); err != nil { return err } @@ -72,11 +73,11 @@ func Cloud(c *types.Config, update bool) error { return err } - if err := InstallVlan(); err != nil { + if err := InstallVlan(distro); err != nil { return err } - if err := ConfigureVLAN(iface); err != nil { + if err := ConfigureVLAN(iface, distro); err != nil { return err } @@ -101,7 +102,7 @@ func Cloud(c *types.Config, update bool) error { if utils.GetLock(3, stack.LocksDir) { fmt.Println("Installing Docker") - if err := InstallDocker(); err != nil { + if err := InstallDocker(distro); err != nil { return err } @@ -145,11 +146,11 @@ func Cloud(c *types.Config, update bool) error { if utils.GetLock(12, stack.LocksDir) || update { fmt.Println("Installing reverse proxy. This may take a while.") - if err := InstallNginx(); err != nil { + if err := InstallNginx(distro); err != nil { return err } - if err := ConfigureNginx(c, stack); err != nil { + if err := ConfigureNginx(c, stack, distro); err != nil { return err } @@ -162,7 +163,7 @@ func Cloud(c *types.Config, update bool) error { if utils.GetLock(5, stack.LocksDir) { fmt.Println("Installing Administration Tools") - if err := InstallTools(); err != nil { + if err := InstallTools(distro); err != nil { return err } diff --git a/installer/docker.go b/installer/docker.go index 437d6d21a..278db7e04 100644 --- a/installer/docker.go +++ b/installer/docker.go @@ -9,33 +9,57 @@ import ( "github.com/utmstack/UTMStack/installer/utils" ) -func InstallDocker() error { - env := []string{"DEBIAN_FRONTEND=noninteractive"} +func InstallDocker(distro string) error { + switch distro { + case "ubuntu": + env := []string{"DEBIAN_FRONTEND=noninteractive"} - if err := utils.RunEnvCmd(env, "apt-get", "update"); err != nil { - return err - } + if err := utils.RunEnvCmd(env, "apt-get", "update"); err != nil { + return err + } - if err := utils.RunEnvCmd(env, "apt-get", "install", "-y", "apt-transport-https", "ca-certificates", "curl", "gnupg-agent", "software-properties-common"); err != nil { - return err - } + if err := utils.RunEnvCmd(env, "apt-get", "install", "-y", "apt-transport-https", "ca-certificates", "curl", "gnupg-agent", "software-properties-common"); err != nil { + return err + } - if err := utils.RunEnvCmd(env, "sh", "-c", "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -"); err != nil { - return err - } + if err := utils.RunEnvCmd(env, "sh", "-c", "curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -"); err != nil { + return err + } - if err := utils.RunEnvCmd(env, "sh", "-c", `add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"`); err != nil { - return err - } + if err := utils.RunEnvCmd(env, "sh", "-c", `add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"`); err != nil { + return err + } - if err := utils.RunEnvCmd(env, "apt-get", "update"); err != nil { - return err - } + if err := utils.RunEnvCmd(env, "apt-get", "update"); err != nil { + return err + } - if err := utils.RunEnvCmd(env, "apt-get", "install", "-y", "docker-ce", "docker-ce-cli", "containerd.io", "docker-compose"); err != nil { - return err - } + if err := utils.RunEnvCmd(env, "apt-get", "install", "-y", "docker-ce", "docker-ce-cli", "containerd.io", "docker-compose"); err != nil { + return err + } + case "redhat": + env := []string{"DNF_YUM_AUTO_YES=1"} + + if err := utils.RunEnvCmd(env, "dnf", "install", "-y", "dnf-plugins-core", "ca-certificates", "curl"); err != nil { + return err + } + + if err := utils.RunEnvCmd(env, "dnf", "config-manager", "--add-repo", "https://download.docker.com/linux/centos/docker-ce.repo"); err != nil { + return err + } + if err := utils.RunEnvCmd(env, "dnf", "makecache"); err != nil { + return err + } + + if err := utils.RunEnvCmd(env, "dnf", "install", "-y", "docker-ce", "docker-ce-cli", "containerd.io", "docker-compose-plugin", "docker-buildx-plugin"); err != nil { + return err + } + + if err := utils.RunEnvCmd(env, "systemctl", "enable", "--now", "docker"); err != nil { + return err + } + } return nil } diff --git a/installer/main.go b/installer/main.go index 6a3464649..fe5316376 100644 --- a/installer/main.go +++ b/installer/main.go @@ -4,8 +4,8 @@ import ( "fmt" "os" - "github.com/utmstack/UTMStack/installer/utils" "github.com/utmstack/UTMStack/installer/types" + "github.com/utmstack/UTMStack/installer/utils" ) func main() { @@ -22,7 +22,8 @@ func main() { } } - if err := utils.CheckDistro("ubuntu"); err != nil { + distro, err := utils.CheckDistro() + if err != nil { fmt.Println(err) os.Exit(1) } @@ -77,13 +78,13 @@ func main() { switch config.ServerType { case "aio": - err := Master(config) + err := Master(config, distro) if err != nil { fmt.Println(err) os.Exit(1) } case "cloud": - err := Cloud(config, update) + err := Cloud(config, update, distro) if err != nil { fmt.Println(err) os.Exit(1) diff --git a/installer/master.go b/installer/master.go index 3e6643eb5..792002cfb 100644 --- a/installer/master.go +++ b/installer/master.go @@ -9,7 +9,7 @@ import ( "github.com/utmstack/UTMStack/installer/utils" ) -func Master(c *types.Config) error { +func Master(c *types.Config, distro string) error { if err := utils.CheckCPU(2); err != nil { return err } @@ -43,7 +43,7 @@ func Master(c *types.Config) error { if utils.GetLock(2, stack.LocksDir) { fmt.Println("Preparing system to run UTMStack") - if err := PrepareSystem(); err != nil { + if err := PrepareSystem(distro); err != nil { return err } @@ -72,11 +72,11 @@ func Master(c *types.Config) error { return err } - if err := InstallVlan(); err != nil { + if err := InstallVlan(distro); err != nil { return err } - if err := ConfigureVLAN(iface); err != nil { + if err := ConfigureVLAN(iface, distro); err != nil { return err } @@ -101,7 +101,7 @@ func Master(c *types.Config) error { if utils.GetLock(3, stack.LocksDir) { fmt.Println("Installing Docker") - if err := InstallDocker(); err != nil { + if err := InstallDocker(distro); err != nil { return err } @@ -138,11 +138,11 @@ func Master(c *types.Config) error { fmt.Println("Installing reverse proxy. This may take a while.") - if err := InstallNginx(); err != nil { + if err := InstallNginx(distro); err != nil { return err } - if err := ConfigureNginx(c, stack); err != nil { + if err := ConfigureNginx(c, stack, distro); err != nil { return err } @@ -150,7 +150,7 @@ func Master(c *types.Config) error { if utils.GetLock(5, stack.LocksDir) { fmt.Println("Installing Administration Tools") - if err := InstallTools(); err != nil { + if err := InstallTools(distro); err != nil { return err } diff --git a/installer/network.go b/installer/network.go index 381f7bb0b..d82486eba 100644 --- a/installer/network.go +++ b/installer/network.go @@ -40,40 +40,71 @@ func checkRenderer() (string, error) { return "networkd", nil } -func ConfigureVLAN(mainIface string) error { - renderer, err := checkRenderer() - if err != nil { - return err - } - +func ConfigureVLAN(mainIface string, distro string) error { c := Vlan{ - Renderer: renderer, - Iface: mainIface, + Iface: mainIface, } - log.Println("Generating vlan config") - err = utils.GenerateConfig(c, templates.Vlan, path.Join("/etc", "netplan", "99-vlan.yaml")) - if err != nil { - return err - } + switch distro { + case "ubuntu": + renderer, err := checkRenderer() + if err != nil { + return err + } + c.Renderer = renderer - log.Println("Applying vlan config") - if err := utils.RunCmd("netplan", "apply"); err != nil { - return err - } + log.Println("Generating vlan config") + err = utils.GenerateConfig(c, templates.VlanUbuntu, path.Join("/etc", "netplan", "99-vlan.yaml")) + if err != nil { + return err + } + + log.Println("Applying vlan config") + if err := utils.RunCmd("netplan", "apply"); err != nil { + return err + } + + case "redhat": + err := utils.GenerateConfig(c, templates.VlanRedHat, "/etc/sysconfig/network-scripts/ifcfg-vlan10") + if err != nil { + return err + } + + if err := utils.RunCmd("modprobe", "8021q"); err != nil { + return err + } + + if err := os.WriteFile("/etc/modules-load.d/8021q.conf", []byte("8021q\n"), 0644); err != nil { + return err + } + + if err := utils.RunCmd("systemctl", "restart", "NetworkManager"); err != nil { + return err + } + } return nil } -func InstallVlan() error { - env := []string{"DEBIAN_FRONTEND=noninteractive"} +func InstallVlan(distro string) error { + switch distro { + case "ubuntu": + env := []string{"DEBIAN_FRONTEND=noninteractive"} - if err := utils.RunEnvCmd(env, "apt-get", "update"); err != nil { - return err - } + if err := utils.RunEnvCmd(env, "apt-get", "update", "-y"); err != nil { + return err + } - if err := utils.RunEnvCmd(env, "apt-get", "install", "-y", "vlan"); err != nil { - return err + if err := utils.RunEnvCmd(env, "apt-get", "install", "-y", "vlan"); err != nil { + return err + } + + case "redhat": + env := []string{"DNF_YUM_AUTO_YES=1"} + + if err := utils.RunEnvCmd(env, "dnf", "update", "-y"); err != nil { + return err + } } return nil diff --git a/installer/nginx.go b/installer/nginx.go index 6cddc9cf6..3691a5b70 100644 --- a/installer/nginx.go +++ b/installer/nginx.go @@ -8,21 +8,30 @@ import ( "github.com/utmstack/UTMStack/installer/utils" ) -func InstallNginx() error { - env := []string{"DEBIAN_FRONTEND=noninteractive"} +func InstallNginx(distro string) error { + switch distro { + case "ubuntu": + env := []string{"DEBIAN_FRONTEND=noninteractive"} - if err := utils.RunEnvCmd(env, "apt-get", "update"); err != nil { - return err - } + if err := utils.RunEnvCmd(env, "apt-get", "update"); err != nil { + return err + } - if err := utils.RunEnvCmd(env, "apt-get", "install", "-y", "nginx"); err != nil { - return err - } + if err := utils.RunEnvCmd(env, "apt-get", "install", "-y", "nginx"); err != nil { + return err + } + + case "redhat": + env := []string{"DNF_YUM_AUTO_YES=1"} + if err := utils.RunEnvCmd(env, "dnf", "install", "-y", "nginx"); err != nil { + return err + } + } return nil } -func ConfigureNginx(conf *types.Config, stack *types.StackConfig) error { +func ConfigureNginx(conf *types.Config, stack *types.StackConfig, distro string) error { c := types.NginxConfig{ SharedKey: conf.InternalKey, } @@ -32,9 +41,26 @@ func ConfigureNginx(conf *types.Config, stack *types.StackConfig) error { return err } - err = utils.GenerateConfig(c, templates.Proxy, path.Join("/", "etc", "nginx", "sites-available", "default")) - if err != nil { - return err + switch distro { + case "ubuntu": + err = utils.GenerateConfig(c, templates.ProxyUbuntu, path.Join("/", "etc", "nginx", "sites-available", "default")) + if err != nil { + return err + } + + case "redhat": + err = utils.GenerateConfig(c, templates.ProxyRHEL, path.Join("/", "etc", "nginx", "nginx.conf")) + if err != nil { + return err + } + + if err := utils.RunCmd("chcon", "-R", "-t", "httpd_sys_content_t", "/utmstack/cert/"); err != nil { + return err + } + + if err := utils.RunCmd("setsebool", "-P", "httpd_read_user_content", "1"); err != nil { + return err + } } if err := utils.RunCmd("systemctl", "restart", "nginx"); err != nil { diff --git a/installer/system.go b/installer/system.go index 10123a13a..e6aa3c95a 100644 --- a/installer/system.go +++ b/installer/system.go @@ -8,7 +8,23 @@ import ( "github.com/utmstack/UTMStack/installer/utils" ) -func PrepareSystem() error { +func PrepareSystem(distro string) error { + if distro == "redhat" { + if err := utils.RunCmd("setenforce", "0"); err != nil { + return fmt.Errorf("failed to disable SELinux immediately: %v", err) + } + + if err := utils.RunCmd("sed", "-i", "s/^SELINUX=.*/SELINUX=disabled/", "/etc/selinux/config"); err != nil { + return fmt.Errorf("failed to configure permanent SELinux setting: %v", err) + } + + if err := utils.RunCmd("systemctl", "disable", "firewalld"); err != nil { + return fmt.Errorf("failed to disable firewalld: %v", err) + } + if err := utils.RunCmd("systemctl", "stop", "firewalld"); err != nil { + return fmt.Errorf("failed to stop firewalld: %v", err) + } + } sysctl := []string{ "vm.max_map_count=262144", "net.ipv6.conf.all.disable_ipv6=1", diff --git a/installer/templates/interfaces.go b/installer/templates/interfaces.go index c22a2f11d..56fec7cb2 100644 --- a/installer/templates/interfaces.go +++ b/installer/templates/interfaces.go @@ -1,6 +1,6 @@ package templates -const Vlan string = ` +const VlanUbuntu string = ` network: version: 2 renderer: {{ .Renderer }} @@ -11,3 +11,14 @@ network: link: {{ .Iface }} addresses: [10.21.199.3/24] ` +const VlanRedHat string = ` +DEVICE=vlan10 +TYPE=Vlan +VLAN_ID=10 +PHYSDEV={{ .Iface }} +BOOTPROTO=none +IPADDR=10.21.199.3 +PREFIX=24 +VLAN=yes +ONBOOT=yes +` diff --git a/installer/templates/proxy.go b/installer/templates/proxy.go index 8170cf87e..9a50b75ad 100644 --- a/installer/templates/proxy.go +++ b/installer/templates/proxy.go @@ -1,6 +1,6 @@ package templates -const Proxy string = `server { +const ProxyUbuntu string = `server { listen 80 default_server; server_name _; return 301 https://$host$request_uri; @@ -34,4 +34,53 @@ server { client_max_body_size 200M; client_body_buffer_size 200M; -}` \ No newline at end of file +}` + +const ProxyRHEL string = `worker_processes auto; + +events { + worker_connections 2048; +} + +http { + include mime.types; + default_type application/octet-stream; + + keepalive_timeout 65; + +server { + listen 80 default_server; + server_name _; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + server_name _; + + set $utmstack http://127.0.0.1:10001; + set $shared_key {{.SharedKey}}; + + location / { + proxy_pass $utmstack; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header x-shared-key $shared_key; + add_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 900; + } + + ssl_certificate /utmstack/cert/utm.crt; + ssl_certificate_key /utmstack/cert/utm.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + chunked_transfer_encoding on; + + client_max_body_size 200M; + client_body_buffer_size 200M; + } +}` diff --git a/installer/tools.go b/installer/tools.go index a3a283981..aca35db06 100644 --- a/installer/tools.go +++ b/installer/tools.go @@ -2,11 +2,18 @@ package main import "github.com/utmstack/UTMStack/installer/utils" -func InstallTools() error { - env := []string{"DEBIAN_FRONTEND=noninteractive"} - err := utils.RunEnvCmd(env, "apt-get", "install", "-y", "cockpit") - if err != nil { - return err +func InstallTools(distro string) error { + switch distro { + case "ubuntu": + env := []string{"DEBIAN_FRONTEND=noninteractive"} + if err := utils.RunEnvCmd(env, "apt-get", "install", "-y", "cockpit"); err != nil { + return err + } + case "redhat": + env := []string{"DNF_YUM_AUTO_YES=1"} + if err := utils.RunEnvCmd(env, "dnf", "install", "-y", "cockpit"); err != nil { + return err + } } return nil diff --git a/installer/utils/requirements.go b/installer/utils/requirements.go index 10826cc7d..f6206b1af 100644 --- a/installer/utils/requirements.go +++ b/installer/utils/requirements.go @@ -45,10 +45,13 @@ func CheckCPU(cores int) error { return nil } -func CheckDistro(distro string) error { +func CheckDistro() (string, error) { info, _ := host.Info() - if info.Platform != distro { - return fmt.Errorf("your Linux distribution (%s) is not %s", info.Platform, distro) + + distro := info.Platform + if distro != "ubuntu" && distro != "redhat" { + return "", fmt.Errorf("your Linux distribution (%s) is not supported. Supported distributions are: %s, %s", + distro, "ubuntu", "redhat") } - return nil + return distro, nil }