diff --git a/modules/reference/services/proxy-server/3proxy-config.nix b/modules/reference/services/proxy-server/3proxy-config.nix index dd4c392c0..ffa9e87ab 100644 --- a/modules/reference/services/proxy-server/3proxy-config.nix +++ b/modules/reference/services/proxy-server/3proxy-config.nix @@ -1,28 +1,45 @@ # Copyright 2024 TII (SSRC) and the Ghaf contributors # SPDX-License-Identifier: Apache-2.0 -{ config, lib, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.ghaf.reference.services.proxy-server; inherit (lib) mkEnableOption mkIf; - # use nix-prefetch-url to calculate sha256 checksum - # TODO The urls should be fetched during boot. The script should be implemented in netvm or adminvm - #pkgs.fetchurl { - # url = "https://endpoints.office.com/endpoints/worldwide?clientrequestid=b10c5ed1-bad1-445f-b386-b919946339a7"; - # sha256 = "1zly0g23vray4wg6fjxxdys6zzksbymlzggbg75jxqcf8g9j6xnw"; - #}; - msEndpointsFile = ./ms_urls.json; - # Read and parse the JSON file - msEndpointsData = builtins.fromJSON (builtins.readFile msEndpointsFile); - - # Extract URLs from the JSON data based on categories - msExtractUrls = map (x: x.urls or [ ]) ( - lib.filter ( - x: x.category == "Optimize" || x.category == "Allow" || x.category == "Default" - ) msEndpointsData - ); - - msUrlsFlattened = builtins.concatLists msExtractUrls ++ [ "microsoft365.com" ]; + proxyUserName = "proxy-user"; + proxyGroupName = "proxy-admin"; + proxyAllowListName = "allowlist.txt"; + proxyWritableAllowListPath = "/etc/${proxyAllowListName}"; + ms-url-fetcher = pkgs.callPackage ./ms_url_fetcher.nix { + allowListPath = proxyWritableAllowListPath; + }; + + _3proxy-restart = pkgs.writeShellApplication { + name = "3proxy-restart"; + runtimeInputs = [ + pkgs.systemd + pkgs.coreutils # Provides 'sleep' and other basic utilities + ]; + text = '' + + sleep 2 + systemctl stop 3proxy.service + echo "Attempting to start 3proxy service" + + # Retry loop for systemctl start 3proxy.service + while ! systemctl is-active --quiet 3proxy.service; do + echo "3proxy is not activated, retrying to start in 5 seconds..." + systemctl start 3proxy.service + sleep 5 + done + + echo "3proxy service successfully started" + ''; + }; tiiUrls = [ #for jira avatars "*.gravatar.com" @@ -48,6 +65,39 @@ let "ghaflogs.vedenemo.dev" "himalia.vedenemo.dev" ]; + + extraMsUrls = [ + "microsoft365.com" + "spoppe-b.azureedge.net" # microsoft365 icons + + ]; + # Concatenate the lists and join with commas + concatenatedUrls = builtins.concatStringsSep "," (tiiUrls ++ ssrcUrls ++ extraMsUrls); + + config_file_content = '' + # log to stdout + log + + nscache 65535 + nscache6 65535 + + auth iponly + #private addresses + deny * * 0.0.0.0/8,127.0.0.0/8,10.0.0.0/8,100.64.0.0/10,172.16.0.0/12,192.168.0.0/16,::,::1,fc00::/7 + + allow * * ${concatenatedUrls} * + #include dynamic whitelist ips + include "${proxyWritableAllowListPath}" + + deny * * * * + maxconn 200 + + proxy -i${netvmAddr} -p${toString cfg.bindPort} + + flush + + ''; + netvmEntry = builtins.filter (x: x.name == "net-vm") config.ghaf.networking.hosts.entries; netvmAddr = lib.head (builtins.map (x: x.ip) netvmEntry); in @@ -62,10 +112,51 @@ in }; config = mkIf cfg.enable { - assertions = [ + assertions = + [ + ]; + # Define a new group for proxy management + users.groups.${proxyGroupName} = { }; # Create a group named proxy-admin - ]; + # Define a new user with a specific username + users.users.${proxyUserName} = { + isNormalUser = true; + description = "Proxy User for managing allowlist and services"; + extraGroups = [ "${proxyGroupName}" ]; # Adding to 'proxy-admin' for specific access + password = "proxy-user"; # Replace with a secure password + }; + + # Set up the permissions for allowlist.txt + environment.etc."${proxyAllowListName}" = { + text = ''''; + user = "${proxyUserName}"; # Owner is proxy-user + group = "${proxyGroupName}"; # Group is proxy-admin + mode = "0660"; # Permissions: read/write for owner/group, no permissions for others + }; + + # Allow proxy-admin group to manage specific systemd services without a password + security = { + polkit = { + enable = true; + debug = true; + # Polkit rules for allowing proxy-user to run proxy related systemctl + # commands without sudo and password requirement + extraConfig = '' + polkit.addRule(function(action, subject) { + if ((action.id == "org.freedesktop.systemd1.manage-units" && + (action.lookup("unit") == "fetchFile.service" || + action.lookup("unit") == "fetchFile.timer" || + action.lookup("unit") == "3proxy.service")) && + subject.user == "${proxyUserName}") { + return polkit.Result.YES; + } + }); + ''; + }; + }; + + environment.systemPackages = [ ms-url-fetcher ]; #Firewall Settings networking = { firewall.enable = true; @@ -75,32 +166,78 @@ in iptables -I INPUT -p udp -s 192.168.100.0/24 --dport ${toString cfg.bindPort} -j ACCEPT ''; }; + # systemd service for fetching the file + systemd.services.fetchFile = { + description = "Fetch a file periodically with retries if internet is available"; + + serviceConfig = { + ExecStart = "${ms-url-fetcher}/bin/ms-url-fetch"; + # Ensure fetchFile starts after the network is up + Type = "simple"; + # Retry until systemctl restart 3proxy succeeds + ExecStartPost = "${_3proxy-restart}/bin/3proxy-restart"; + # Restart policy on failure + Restart = "on-failure"; # Restart the service if it fails + RestartSec = "10s"; # Wait 10 seconds before restarting + User = "${proxyUserName}"; + }; + }; + + # systemd timer to trigger the service every 10 minutes + systemd.timers.fetchFile = { + description = "Run fetch-file periodically"; + wantedBy = [ "timers.target" ]; + timerConfig = { + User = "${proxyUserName}"; + Persistent = true; # Ensures the timer runs after a system reboot + OnCalendar = "hourly"; # Set to your desired schedule + OnBootSec = "60s"; + }; + }; + + systemd.services."3proxy".serviceConfig = { + RestartSec = "5s"; + User = "${proxyUserName}"; + Group = "proxy-admin"; + }; + services._3proxy = { enable = true; - services = [ - { - type = "proxy"; - bindAddress = "${netvmAddr}"; - inherit (cfg) bindPort; - maxConnections = 200; - auth = [ "iponly" ]; - acl = [ - { - rule = "allow"; - targets = msUrlsFlattened; - } - { - rule = "allow"; - targets = tiiUrls; - } - { - rule = "allow"; - targets = ssrcUrls; - } - { rule = "deny"; } - ]; - } - ]; + # Prepend the 'include' directive before the rest of the configuration + confFile = pkgs.writeText "3proxy.conf" '' + ${config_file_content} + ''; + + /* + NOTE allow and deny configurations should must be placed before the other configs + it is not possible to do with extraConfig. Because it appends the file + */ + /* + services = [ + { + type = "proxy"; + bindAddress = "${netvmAddr}"; + inherit (cfg) bindPort; + maxConnections = 200; + auth = [ "iponly" ]; + acl = [ + { + rule = "allow"; + targets = tiiUrls; + } + { + rule = "allow"; + targets = ssrcUrls; + } + { + rule = "allow"; + targets = extraMsUrls; + } + { rule = "deny"; } + ]; + } + ]; + */ }; }; diff --git a/modules/reference/services/proxy-server/ms_url_fetcher.nix b/modules/reference/services/proxy-server/ms_url_fetcher.nix new file mode 100644 index 000000000..f4832c88f --- /dev/null +++ b/modules/reference/services/proxy-server/ms_url_fetcher.nix @@ -0,0 +1,74 @@ +{ + writeShellApplication, + lib, + pkgs, + allowListPath, + ... +}: +let + url = "https://endpoints.office.com/endpoints/worldwide?clientrequestid=b10c5ed1-bad1-445f-b386-b919946339a7"; + logTag = "ms-url-fetcher"; +in +writeShellApplication { + name = "ms-url-fetch"; + runtimeInputs = [ + pkgs.inetutils + pkgs.curl + pkgs.jq + ]; + text = '' + # Function to write to the allow list + write_to_allow_list() { + + local processedUrls="$1" + local allowListPath="$2" + + { + printf "allow * * " || { logger -t ms-url-fetcher "Failed to print prefix"; return 1; } + echo "$processedUrls" || { logger -t ms-url-fetcher "Failed to echo processed URLs"; return 1; } + } > "$allowListPath" || { logger -t ms-url-fetcher "Failed to write to $allowListPath"; return 2; } + return 0 # Indicate success + } + # Check if the device is connected to the internet. + if ping -c 1 8.8.8.8 &> /dev/null; then + logger -t ${logTag} "Fetching the Microsoft URLs from ${url}" + + # Fetch the JSON file using curl with retry logic + if curl_output=$(curl -s --retry 5 --retry-delay 10 --retry-connrefused "${url}"); then + msurl_output=$(echo "$curl_output" | jq -r '.[]? | select(.category == "Optimize" or .category == "Allow" or .category == "Default") | .urls[]?' | sort | uniq) + # Check if msurl_output is empty + if [ -z "$msurl_output" ]; then + logger -t ${logTag} "No valid URLs found in the fetched data." + exit 4 # No URLs found error + fi + + # Convert the list of URLs into a comma-separated format and save to allowListPath + processedUrls=$(echo "$msurl_output" | tr '\n' ',' | sed 's/,$//'); + + + + # Add the prefix once and save to allowListPath + if write_to_allow_list "$processedUrls" "${allowListPath}"; then + logger -t ${logTag} "Microsoft URLs fetched and saved to ${allowListPath} successfully" + exit 0 # Success exit code + else + logger -t ${logTag} "Failed to process Microsoft URLs with jq" + exit 2 # JQ processing error + fi + else + logger -t ${logTag} "Failed to fetch Microsoft URLs after multiple attempts" + exit 1 # Curl fetching error + fi + else + logger -t ${logTag} "No internet connection. Microsoft URLs not fetched." + exit 3 # No internet connection error + fi + ''; + meta = with lib; { + description = " + The application is a shell script designed to fetch a list of Microsoft URLs + from a specified endpoint and save them to an allow list file. The script includes error + handling and retry logic to ensure robustness in various network conditions. + "; + }; +}