{ config, pkgs, lib, ... }: with lib; let cfg = config.services.naxos.shares.samba; in { options.services.naxos.shares.samba = { enable = mkEnableOption "NaxOS Declarative Samba (SMB) Service"; workgroup = mkOption { type = types.str; default = "WORKGROUP"; description = "NetBIOS workgroup."; }; serverString = mkOption { type = types.str; default = "NaxOS Storage Appliance"; description = "Server announcement string."; }; netbiosName = mkOption { type = types.str; default = "naxos"; description = "NetBIOS hostname."; }; allowedHosts = mkOption { type = types.listOf types.str; default = [ "10.0.0." "192.168." "172.16." "127.0.0.1" "localhost" ]; description = "Allowed IP subnets or hosts for SMB access."; }; shares = mkOption { type = types.attrsOf (types.submodule { options = { enable = mkOption { type = types.bool; default = true; description = "Whether to publish this share."; }; path = mkOption { type = types.str; description = "Target local directory or ZFS mountpoint."; }; comment = mkOption { type = types.str; default = ""; description = "Share description."; }; readOnly = mkOption { type = types.bool; default = false; description = "Whether the share is read-only."; }; browseable = mkOption { type = types.bool; default = true; description = "Whether the share is visible in network browsing."; }; guestOk = mkOption { type = types.bool; default = false; description = "Whether anonymous/guest access is allowed."; }; validUsers = mkOption { type = types.listOf types.str; default = []; description = "List of valid users or @groups."; }; forceUser = mkOption { type = types.nullOr types.str; default = null; description = "Force UNIX user for all connections."; }; forceGroup = mkOption { type = types.nullOr types.str; default = null; description = "Force UNIX group for all connections."; }; createMask = mkOption { type = types.str; default = "0664"; description = "File creation permission mask."; }; directoryMask = mkOption { type = types.str; default = "0775"; description = "Directory creation permission mask."; }; timeMachine = mkOption { type = types.bool; default = false; description = "Enable Apple Time Machine compatibility mode (vfs_fruit)."; }; timeMachineMaxSize = mkOption { type = types.nullOr types.str; default = null; example = "512G"; description = "Quota size limit advertised to Time Machine."; }; }; }); default = {}; description = "Declaratively managed SMB shares."; }; }; config = mkIf cfg.enable { services.samba = { enable = true; openFirewall = true; settings = { global = { workgroup = cfg.workgroup; "server string" = cfg.serverString; "netbios name" = cfg.netbiosName; security = "user"; "hosts allow" = concatStringsSep " " cfg.allowedHosts; "hosts deny" = "0.0.0.0/0"; "guest account" = "nobody"; "map to guest" = "bad user"; # Apple & ZFS ACL compatibility "vfs objects" = "catia fruit streams_xattr acl_xattr"; "fruit:aapl" = "yes"; "fruit:metadata" = "stream"; "fruit:model" = "Macmini"; "fruit:posix_rename" = "yes"; "fruit:zero_file_id" = "yes"; # ACLs and inheritance "map acl inherit" = "yes"; "inherit acls" = "yes"; "ea support" = "yes"; }; } // (mapAttrs' (shareName: shareCfg: nameValuePair shareName ( { path = shareCfg.path; comment = shareCfg.comment; browseable = if shareCfg.browseable then "yes" else "no"; "read only" = if shareCfg.readOnly then "yes" else "no"; "guest ok" = if shareCfg.guestOk then "yes" else "no"; "create mask" = shareCfg.createMask; "directory mask" = shareCfg.directoryMask; } // optionalAttrs (shareCfg.validUsers != []) { "valid users" = concatStringsSep " " shareCfg.validUsers; } // optionalAttrs (shareCfg.forceUser != null) { "force user" = shareCfg.forceUser; } // optionalAttrs (shareCfg.forceGroup != null) { "force group" = shareCfg.forceGroup; } // optionalAttrs shareCfg.timeMachine { "vfs objects" = "catia fruit streams_xattr acl_xattr"; "fruit:time machine" = "yes"; } // optionalAttrs (shareCfg.timeMachine && shareCfg.timeMachineMaxSize != null) { "fruit:time machine max size" = shareCfg.timeMachineMaxSize; } ) ) (filterAttrs (n: v: v.enable) cfg.shares)); }; # Enable Avahi (mDNS / Bonjour) for automatic SMB & Time Machine discovery on macOS/iOS/Windows services.avahi = { enable = true; nssmdns4 = true; publish = { enable = true; userServices = true; }; extraServiceFiles = { smb = '' %h (NaxOS SMB) _smb._tcp 445 _device-info._tcp 0 model=RackMac ''; }; }; }; }