about summary refs log tree commit diff
diff options
context:
space:
mode:
authorazahi <azat@bahawi.net>2025-01-24 04:11:25 +0300
committerazahi <azat@bahawi.net>2025-01-24 04:11:25 +0300
commit865264328824e329d4d706af6bc370199ed2b188 (patch)
treec2d5b05e5b2abee8730f0dfbe35414c8a5d94a00
parent2025-01-21 (diff)
2025-01-24 HEAD master
Diffstat (limited to '')
-rw-r--r--modules/common/documentation.nix17
-rw-r--r--modules/piracy/default.nix3
-rw-r--r--modules/profiles/headful.nix1
-rw-r--r--modules/soju.nix183
-rw-r--r--modules/throttled.nix105
-rw-r--r--overlays.nix21
-rw-r--r--packages/soju-upload.patch222
7 files changed, 360 insertions, 192 deletions
diff --git a/modules/common/documentation.nix b/modules/common/documentation.nix
index f9e0fcb..b28988d 100644
--- a/modules/common/documentation.nix
+++ b/modules/common/documentation.nix
@@ -7,23 +7,28 @@
 }:
 {
   config = {
-    hm.manual = {
-      manpages.enable = this.isHeadful;
-      html.enable = false;
-      json.enable = false;
+    hm = {
+      manual = {
+        manpages.enable = this.isHeadful;
+        html.enable = false;
+        json.enable = false;
+      };
+
+      # Fixes a wierd issue with `direnv` and an unbound value.
+      home.sessionVariables.MANPATH = "";
     };
 
     documentation = {
       enable = this.isHeadful;
       dev.enable = true;
       doc.enable = false;
-      info.enable = false;
+      info.enable = true;
       nixos.enable = true;
 
       man.man-db.manualPages =
         (pkgs.buildEnv {
           name = "man-paths";
-          paths = with config; environment.systemPackages ++ hm.home.packages;
+          paths = config.environment.systemPackages ++ config.hm.home.packages;
           pathsToLink = [ "/share/man" ];
           extraOutputsToInstall = [ "man" ];
           ignoreCollisions = true;
diff --git a/modules/piracy/default.nix b/modules/piracy/default.nix
index 7682356..3554a02 100644
--- a/modules/piracy/default.nix
+++ b/modules/piracy/default.nix
@@ -55,6 +55,9 @@ in
             user = "rtorrent";
             inherit (cfg) group;
 
+            port = 1337;
+            openFirewall = true;
+
             rpcSocket = socket;
             configText =
               with config.services.rtorrent;
diff --git a/modules/profiles/headful.nix b/modules/profiles/headful.nix
index 186d97d..a315af4 100644
--- a/modules/profiles/headful.nix
+++ b/modules/profiles/headful.nix
@@ -54,6 +54,7 @@ in
         packages = with pkgs; [
           anki
           audacity
+          ayugram-desktop
           byedpi
           eaglemode
           easyeffects
diff --git a/modules/soju.nix b/modules/soju.nix
index 2060eca..dbf069d 100644
--- a/modules/soju.nix
+++ b/modules/soju.nix
@@ -12,24 +12,30 @@ in
   options.nixfiles.modules.soju = {
     enable = mkEnableOption "soju";
 
-    address = mkOption {
-      description = "Address.";
-      type = with types; str;
-      default = "";
-    };
-
     port = mkOption {
       description = "Port.";
       type = with types; port;
       default = 6697;
     };
 
+    httpPort = mkOption {
+      description = "HTTP Port.";
+      type = with types; port;
+      default = 9981;
+    };
+
     domain = mkOption {
       description = "Domain.";
       type = with types; str;
       default = config.networking.fqdn;
     };
 
+    uploadsDir = mkOption {
+      description = "Uploads directory.";
+      type = with types; str;
+      default = "/srv/soju/uploads";
+    };
+
     prometheus = {
       enable = mkEnableOption "Prometheus exporter" // {
         default = true;
@@ -50,7 +56,25 @@ in
     mkIf cfg.enable {
       nixfiles.modules = {
         acme.enable = true;
-        nginx.enable = true;
+        nginx = {
+          enable = true;
+          upstreams.soju.servers."127.0.0.1:${toString cfg.httpPort}" = { };
+          virtualHosts.${cfg.domain}.locations = {
+            "/_irc" = {
+              proxyPass = "http://soju";
+              proxyWebsockets = true;
+              extraConfig = ''
+                rewrite ^/_irc/(.*)$ /$1 break;
+              '';
+            };
+            "/_irc/uploads" = {
+              root = "/srv/soju";
+              extraConfig = ''
+                rewrite ^/_irc/(.*)$ /$1 break;
+              '';
+            };
+          };
+        };
         postgresql = {
           enable = true;
           extraPostStart = [
@@ -71,76 +95,87 @@ in
         ];
       };
 
-      systemd.services.soju = {
-        description = "soju IRC bouncer";
-        wantedBy = [ "multi-user.target" ];
-        wants = [ "network-online.target" ];
-        requires = [ "postgresql.service" ];
-        after = [
-          "network-online.target"
-          "postgresql.service"
-        ];
-        serviceConfig = {
-          ExecStart =
-            let
-              # https://soju.im/doc/soju.1.html
-              configFile = pkgs.writeText "soju.conf" ''
-                listen ircs://${cfg.address}:${toString cfg.port}
-                tls ${with config.certs.${cfg.domain}; "${directory}/fullchain.pem ${directory}/key.pem"}
-                ${with cfg.prometheus; optionalString enable "listen http+prometheus://localhost:${toString port}"}
-                db postgres "${
-                  concatStringsSep " " [
-                    "host=/run/postgresql"
-                    "user=${db}"
-                    "dbname=${db}"
-                    "sslmode=disable"
-                  ]
-                }"
-                message-store db
-                hostname ${cfg.domain}
-                title ${cfg.domain}
-              '';
-            in
-            concatStringsSep " " [
-              (getExe' pkgs.soju "soju")
-              "-config ${configFile}"
-            ];
-          DynamicUser = true;
-          SupplementaryGroups = [ config.services.nginx.group ];
-          AmbientCapabilities = [ "" ];
-          CapabilityBoundingSet = [ "" ];
-          UMask = "0077";
-          LockPersonality = true;
-          MemoryDenyWriteExecute = true;
-          NoNewPrivileges = true;
-          PrivateDevices = true;
-          PrivateTmp = true;
-          PrivateUsers = true;
-          ProtectClock = true;
-          ProtectControlGroups = true;
-          ProtectHome = true;
-          ProtectHostname = true;
-          ProtectKernelLogs = true;
-          ProtectKernelModules = true;
-          ProtectKernelTunables = true;
-          ProtectSystem = "strict";
-          ProtectProc = "invisible";
-          ProcSubset = "pid";
-          RemoveIPC = true;
-          RestrictAddressFamilies = [
-            "AF_UNIX"
-            "AF_INET"
-            "AF_INET6"
+      systemd = {
+        services.soju = {
+          description = "soju IRC bouncer";
+          documentation = [
+            "https://soju.im/"
+            "man:soju(1)"
+            "man:sojuctl(1)"
           ];
-          RestrictNamespaces = true;
-          RestrictRealtime = true;
-          RestrictSUIDSGID = true;
-          SystemCallArchitectures = "native";
-          SystemCallFilter = [
-            "@system-service"
-            "~@privileged"
+          wantedBy = [ "multi-user.target" ];
+          wants = [ "network-online.target" ];
+          requires = [ "postgresql.service" ];
+          after = [
+            "network-online.target"
+            "postgresql.service"
           ];
+          serviceConfig = {
+            ExecStart =
+              let
+                # https://soju.im/doc/soju.1.html
+                configFile = pkgs.writeText "soju.conf" ''
+                  listen ircs://:${toString cfg.port}
+                  listen http://localhost:${toString cfg.httpPort}
+                  tls ${with config.certs.${cfg.domain}; "${directory}/fullchain.pem ${directory}/key.pem"}
+                  ${with cfg.prometheus; optionalString enable "listen http+prometheus://localhost:${toString port}"}
+                  db postgres "${
+                    concatStringsSep " " [
+                      "host=/run/postgresql"
+                      "user=${db}"
+                      "dbname=${db}"
+                      "sslmode=disable"
+                    ]
+                  }"
+                  message-store db
+                  file-upload fs ${cfg.uploadsDir}
+                  hostname ${cfg.domain}
+                  http-ingress https://${cfg.domain}/_irc
+                '';
+              in
+              "${pkgs.soju}/bin/soju -config ${configFile}";
+            ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+            DynamicUser = true;
+            ReadWritePaths = [ cfg.uploadsDir ];
+            SupplementaryGroups = [ config.services.nginx.group ];
+            AmbientCapabilities = [ "" ];
+            CapabilityBoundingSet = [ "" ];
+            LockPersonality = true;
+            MemoryDenyWriteExecute = true;
+            NoNewPrivileges = true;
+            PrivateDevices = true;
+            PrivateTmp = true;
+            PrivateUsers = true;
+            ProtectClock = true;
+            ProtectControlGroups = true;
+            ProtectHome = true;
+            ProtectHostname = true;
+            ProtectKernelLogs = true;
+            ProtectKernelModules = true;
+            ProtectKernelTunables = true;
+            ProtectSystem = "strict";
+            ProtectProc = "invisible";
+            ProcSubset = "pid";
+            RemoveIPC = true;
+            RestrictAddressFamilies = [
+              "AF_UNIX"
+              "AF_INET"
+              "AF_INET6"
+            ];
+            RestrictNamespaces = true;
+            RestrictRealtime = true;
+            RestrictSUIDSGID = true;
+            SystemCallArchitectures = "native";
+            SystemCallFilter = [
+              "@system-service"
+              "~@privileged"
+            ];
+          };
         };
+
+        tmpfiles.rules = [
+          "d ${cfg.uploadsDir} 0755 soju soju"
+        ];
       };
     };
 }
diff --git a/modules/throttled.nix b/modules/throttled.nix
deleted file mode 100644
index 7d37cd4..0000000
--- a/modules/throttled.nix
+++ /dev/null
@@ -1,105 +0,0 @@
-{ config, lib, ... }:
-with lib;
-let
-  cfg = config.nixfiles.modules.throttled;
-in
-{
-  options.nixfiles.modules.throttled.enable = mkEnableOption "Throttled";
-
-  config = mkIf cfg.enable {
-    services.throttled.enable = true;
-
-    environment.etc."throttled.conf".text = mkDefault ''
-      [GENERAL]
-      # Enable or disable the script execution
-      Enabled: True
-      # SYSFS path for checking if the system is running on AC power
-      Sysfs_Power_Path: /sys/class/power_supply/AC*/online
-      # Auto reload config on changes
-      Autoreload: True
-
-      ## Settings to apply while connected to Battery power
-      [BATTERY]
-      # Update the registers every this many seconds
-      Update_Rate_s: 30
-      # Max package power for time window #1
-      PL1_Tdp_W: 29
-      # Time window #1 duration
-      PL1_Duration_s: 28
-      # Max package power for time window #2
-      PL2_Tdp_W: 44
-      # Time window #2 duration
-      PL2_Duration_S: 0.002
-      # Max allowed temperature before throttling
-      Trip_Temp_C: 85
-      # Set cTDP to normal=0, down=1 or up=2 (EXPERIMENTAL)
-      cTDP: 0
-      # Disable BDPROCHOT (EXPERIMENTAL)
-      Disable_BDPROCHOT: False
-
-      ## Settings to apply while connected to AC power
-      [AC]
-      # Update the registers every this many seconds
-      Update_Rate_s: 5
-      # Max package power for time window #1
-      PL1_Tdp_W: 44
-      # Time window #1 duration
-      PL1_Duration_s: 28
-      # Max package power for time window #2
-      PL2_Tdp_W: 44
-      # Time window #2 duration
-      PL2_Duration_S: 0.002
-      # Max allowed temperature before throttling
-      Trip_Temp_C: 95
-      # Set HWP energy performance hints to 'performance' on high load (EXPERIMENTAL)
-      # Uncomment only if you really want to use it
-      # HWP_Mode: False
-      # Set cTDP to normal=0, down=1 or up=2 (EXPERIMENTAL)
-      cTDP: 0
-      # Disable BDPROCHOT (EXPERIMENTAL)
-      Disable_BDPROCHOT: False
-
-      # All voltage values are expressed in mV and *MUST* be negative (i.e. undervolt)!
-      [UNDERVOLT.BATTERY]
-      # CPU core voltage offset (mV)
-      CORE: 0
-      # Integrated GPU voltage offset (mV)
-      GPU: 0
-      # CPU cache voltage offset (mV)
-      CACHE: 0
-      # System Agent voltage offset (mV)
-      UNCORE: 0
-      # Analog I/O voltage offset (mV)
-      ANALOGIO: 0
-
-      # All voltage values are expressed in mV and *MUST* be negative (i.e. undervolt)!
-      [UNDERVOLT.AC]
-      # CPU core voltage offset (mV)
-      CORE: 0
-      # Integrated GPU voltage offset (mV)
-      GPU: 0
-      # CPU cache voltage offset (mV)
-      CACHE: 0
-      # System Agent voltage offset (mV)
-      UNCORE: 0
-      # Analog I/O voltage offset (mV)
-      ANALOGIO: 0
-
-      # [ICCMAX.AC]
-      # # CPU core max current (A)
-      # CORE:
-      # # Integrated GPU max current (A)
-      # GPU:
-      # # CPU cache max current (A)
-      # CACHE:
-
-      # [ICCMAX.BATTERY]
-      # # CPU core max current (A)
-      # CORE:
-      # # Integrated GPU max current (A)
-      # GPU:
-      # # CPU cache max current (A)
-      # CACHE:
-    '';
-  };
-}
diff --git a/overlays.nix b/overlays.nix
index f5c3aac..3c64def 100644
--- a/overlays.nix
+++ b/overlays.nix
@@ -16,6 +16,7 @@
       '';
 
       grc.__output = {
+        version.__assign = "nixfiles";
         src.__assign = final.fetchFromGitHub {
           owner = "garabik";
           repo = "grc";
@@ -63,13 +64,6 @@
 
       openssl_1_0_0.__assign = prev.callPackage ./packages/openssl_1_0_0.nix { };
 
-      telegram-desktop = {
-        __input.stdenv = final.useMoldLinker;
-        # __output.patches.__append = [
-        #   ./packages/telegram-no-ads.patch
-        # ];
-      };
-
       vesktop = {
         __input = {
           withMiddleClickScroll.__assign = true;
@@ -92,5 +86,18 @@
 
       dendrite =
         _: (lib.packages.fromPR 366129 "sha256-oI9Afm3azJyEz4SJJIwuzeyuH7IaiGNTSA442vFlfv4=").dendrite;
+
+      soju.__output = {
+        version.__assign = "nixfiles";
+        src.__assign = final.fetchFromGitea {
+          domain = "codeberg.org";
+          owner = "emersion";
+          repo = "soju";
+          rev = "5f68e69529d8da21b22d073f07278722488d507d";
+          hash = "sha256-oJDw2AzUBfU2KkG4YzzAXiE5UZj0m0/zw/GuHVrqddM=";
+        };
+        vendorHash.__assign = "sha256-tq9FI8A3pi3ztcLYF6sZ4wmwTD0HWq4g2EAl7eLo+po=";
+        patches.__append = [ ./packages/soju-upload.patch ];
+      };
     };
 }
diff --git a/packages/soju-upload.patch b/packages/soju-upload.patch
new file mode 100644
index 0000000..03efe76
--- /dev/null
+++ b/packages/soju-upload.patch
@@ -0,0 +1,222 @@
+diff --git i/fileupload/fileupload.go w/fileupload/fileupload.go
+index 07fccf1..55104f8 100644
+--- i/fileupload/fileupload.go
++++ w/fileupload/fileupload.go
+@@ -7,6 +7,7 @@ import (
+ 	"errors"
+ 	"fmt"
+ 	"io"
++	"log"
+ 	"mime"
+ 	"net/http"
+ 	"net/url"
+@@ -153,30 +154,35 @@ func (h *Handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
+ 		return
+ 	}
+ 
++	var err error
+ 	switch req.Method {
+ 	case http.MethodOptions:
+ 		resp.WriteHeader(http.StatusNoContent)
+ 	case http.MethodHead, http.MethodGet:
+-		h.fetch(resp, req)
++		err = h.fetch(resp, req)
+ 	case http.MethodPost:
+-		h.store(resp, req)
++		err = h.store(resp, req)
+ 	default:
+ 		http.Error(resp, "only OPTIONS, HEAD, GET and POST are allowed", http.StatusMethodNotAllowed)
++		err = errors.New(http.StatusText(http.StatusMethodNotAllowed))
++	}
++	if err != nil {
++		log.Printf("upload: %s", err.Error())
+ 	}
+ }
+ 
+-func (h *Handler) fetch(resp http.ResponseWriter, req *http.Request) {
++func (h *Handler) fetch(resp http.ResponseWriter, req *http.Request) error {
+ 	prefix := "/uploads/"
+ 	if !strings.HasPrefix(req.URL.Path, prefix) {
+ 		http.Error(resp, "invalid path", http.StatusNotFound)
+-		return
++		return fmt.Errorf("invalid path")
+ 	}
+ 
+ 	filename := strings.TrimPrefix(req.URL.Path, prefix)
+ 	filename = path.Join("/", filename)[1:] // prevent directory traversal
+ 	if filename == "" {
+ 		http.Error(resp, "invalid path", http.StatusNotFound)
+-		return
++		return fmt.Errorf("invalid path")
+ 	}
+ 
+ 	basename, modTime, content, err := h.Uploader.load(req.Context(), filename)
+@@ -184,10 +190,10 @@ func (h *Handler) fetch(resp http.ResponseWriter, req *http.Request) {
+ 		var httpErr *httpError
+ 		if errors.As(err, &httpErr) {
+ 			httpErr.Write(resp)
+-			return
++			return err
+ 		}
+ 		http.Error(resp, "failed to open file", http.StatusNotFound)
+-		return
++		return fmt.Errorf("failed to open file: %v", err)
+ 	}
+ 	defer content.Close()
+ 
+@@ -200,7 +206,7 @@ func (h *Handler) fetch(resp http.ResponseWriter, req *http.Request) {
+ 		_, err := content.Seek(0, io.SeekStart) // rewind to output whole file
+ 		if err != nil {
+ 			http.Error(resp, "failed to seek file", http.StatusInternalServerError)
+-			return
++			return fmt.Errorf("failed to seek file: %v", err)
+ 		}
+ 	}
+ 
+@@ -219,18 +225,20 @@ func (h *Handler) fetch(resp http.ResponseWriter, req *http.Request) {
+ 	resp.Header().Set("Content-Disposition", contentDisp)
+ 
+ 	http.ServeContent(resp, req, basename, modTime, content)
++
++	return nil
+ }
+ 
+-func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
++func (h *Handler) store(resp http.ResponseWriter, req *http.Request) error {
+ 	if req.URL.Path != "/uploads" {
+ 		http.Error(resp, "invalid path", http.StatusNotFound)
+-		return
++		return fmt.Errorf("invalid path")
+ 	}
+ 
+ 	authz := req.Header.Get("Authorization")
+ 	if authz == "" {
+ 		http.Error(resp, "missing Authorization header", http.StatusUnauthorized)
+-		return
++		return fmt.Errorf("missing Authorization header")
+ 	}
+ 
+ 	var (
+@@ -243,26 +251,26 @@ func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
+ 		plainAuth := h.Auth.Plain
+ 		if plainAuth == nil {
+ 			http.Error(resp, "Basic scheme in Authorization header not supported", http.StatusBadRequest)
+-			return
++			return fmt.Errorf("Basic scheme in Authorization header not supported")
+ 		}
+ 		var password string
+ 		var ok bool
+ 		username, password, ok = req.BasicAuth()
+ 		if !ok {
+ 			http.Error(resp, "invalid Authorization header", http.StatusBadRequest)
+-			return
++			return fmt.Errorf("invalid Authorization header")
+ 		}
+ 		err = plainAuth.AuthPlain(req.Context(), h.DB, username, password)
+ 	case "bearer":
+ 		oauthAuth := h.Auth.OAuthBearer
+ 		if oauthAuth == nil {
+ 			http.Error(resp, "Bearer scheme in Authorization header not supported", http.StatusBadRequest)
+-			return
++			return fmt.Errorf("Bearer scheme in Authorization header not supported")
+ 		}
+ 		username, err = oauthAuth.AuthOAuthBearer(req.Context(), h.DB, param)
+ 	default:
+ 		http.Error(resp, "unsupported Authorization header scheme", http.StatusBadRequest)
+-		return
++		return fmt.Errorf("unsupported Authorization header scheme")
+ 	}
+ 	if err != nil {
+ 		var msg string
+@@ -272,7 +280,7 @@ func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
+ 			msg = "authentication failed"
+ 		}
+ 		http.Error(resp, msg, http.StatusForbidden)
+-		return
++		return fmt.Errorf("%s: %v", msg, err)
+ 	}
+ 
+ 	var mimeType string
+@@ -284,7 +292,7 @@ func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
+ 		mimeType, params, err = mime.ParseMediaType(contentType)
+ 		if err != nil {
+ 			http.Error(resp, "failed to parse Content-Type", http.StatusBadRequest)
+-			return
++			return fmt.Errorf("failed to parse Content-Type: %v", err)
+ 		}
+ 		if mimeType == "application/octet-stream" {
+ 			mimeType = ""
+@@ -295,7 +303,7 @@ func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
+ 			// OK
+ 		default:
+ 			http.Error(resp, "unsupported charset", http.StatusUnsupportedMediaType)
+-			return
++			return fmt.Errorf("unsupported charset")
+ 		}
+ 	}
+ 
+@@ -304,7 +312,7 @@ func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
+ 		_, params, err := mime.ParseMediaType(contentDisp)
+ 		if err != nil {
+ 			http.Error(resp, "failed to parse Content-Disposition", http.StatusBadRequest)
+-			return
++			return fmt.Errorf("failed to parse Content-Disposition: %v", err)
+ 		}
+ 		basename = path.Base(params["filename"])
+ 	}
+@@ -325,7 +333,7 @@ func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
+ 		var httpErr *httpError
+ 		if errors.As(err, &httpErr) {
+ 			httpErr.Write(resp)
+-			return
++			return err
+ 		}
+ 		status := http.StatusInternalServerError
+ 		var maxBytesErr *http.MaxBytesError
+@@ -335,19 +343,21 @@ func (h *Handler) store(resp http.ResponseWriter, req *http.Request) {
+ 			resp.Header().Set("Upload-Limit", fmt.Sprintf("maxsize=%v", maxBytesErr.Limit))
+ 		}
+ 		http.Error(resp, "failed to write file", status)
+-		return
++		return fmt.Errorf("failed to write file: %v", err)
+ 	}
+ 
+ 	u, err := url.Parse(out)
+ 	if err != nil {
+ 		http.Error(resp, "failed to write file", http.StatusInternalServerError)
+-		return
++		return fmt.Errorf("failed to write file: %v", err)
+ 	}
+-	baseURL := url.URL{Path: "/uploads/"}
++	baseURL := url.URL{Path: "/_irc/uploads/"}
+ 	out = baseURL.ResolveReference(u).String()
+ 
+ 	resp.Header().Set("Location", out)
+ 	resp.WriteHeader(http.StatusCreated)
++
++	return nil
+ }
+ 
+ func generateToken(n int) (string, error) {
+diff --git i/fileupload/fs.go w/fileupload/fs.go
+index c0c401d..66337a1 100644
+--- i/fileupload/fs.go
++++ w/fileupload/fs.go
+@@ -56,7 +56,7 @@ func (fs *fileuploadFS) store(ctx context.Context, r io.Reader, username, mimeTy
+ 	}
+ 
+ 	dir := filepath.Join(fs.dir, username)
+-	if err := os.MkdirAll(dir, 0700); err != nil {
++	if err := os.MkdirAll(dir, 0755); err != nil {
+ 		return "", fmt.Errorf("failed to create user upload directory: %w", err)
+ 	}
+ 
+@@ -77,7 +77,7 @@ func (fs *fileuploadFS) store(ctx context.Context, r io.Reader, username, mimeTy
+ 		}
+ 		basename += suffix
+ 
+-		f, err = os.OpenFile(filepath.Join(dir, basename), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
++		f, err = os.OpenFile(filepath.Join(dir, basename), os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
+ 		if err == nil {
+ 			break
+ 		} else if !os.IsExist(err) {

Consider giving Nix/NixOS a try! <3