1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8
9 pkg = config.hardware.sane.backends-package.override {
10 scanSnapDriversUnfree = config.hardware.sane.drivers.scanSnap.enable;
11 scanSnapDriversPackage = config.hardware.sane.drivers.scanSnap.package;
12 };
13
14 sanedConf = pkgs.writeTextFile {
15 name = "saned.conf";
16 destination = "/etc/sane.d/saned.conf";
17 text = ''
18 localhost
19 ${config.services.saned.extraConfig}
20 '';
21 };
22
23 netConf = pkgs.writeTextFile {
24 name = "net.conf";
25 destination = "/etc/sane.d/net.conf";
26 text = ''
27 ${lib.optionalString config.services.saned.enable "localhost"}
28 ${config.hardware.sane.netConf}
29 '';
30 };
31
32 env = {
33 SANE_CONFIG_DIR = "/etc/sane-config";
34 LD_LIBRARY_PATH = [ "/etc/sane-libs" ];
35 };
36
37 backends =
38 [
39 pkg
40 netConf
41 ]
42 ++ lib.optional config.services.saned.enable sanedConf
43 ++ config.hardware.sane.extraBackends;
44 saneConfig = pkgs.mkSaneConfig {
45 paths = backends;
46 inherit (config.hardware.sane) disabledDefaultBackends;
47 };
48
49 enabled = config.hardware.sane.enable || config.services.saned.enable;
50
51in
52
53{
54
55 ###### interface
56
57 options = {
58
59 hardware.sane.enable = lib.mkOption {
60 type = lib.types.bool;
61 default = false;
62 description = ''
63 Enable support for SANE scanners.
64
65 ::: {.note}
66 Users in the "scanner" group will gain access to the scanner, or the "lp" group if it's also a printer.
67 :::
68 '';
69 };
70
71 hardware.sane.backends-package = lib.mkOption {
72 type = lib.types.package;
73 default = pkgs.sane-backends;
74 defaultText = lib.literalExpression "pkgs.sane-backends";
75 description = "Backends driver package to use.";
76 };
77
78 hardware.sane.snapshot = lib.mkOption {
79 type = lib.types.bool;
80 default = false;
81 description = "Use a development snapshot of SANE scanner drivers.";
82 };
83
84 hardware.sane.extraBackends = lib.mkOption {
85 type = lib.types.listOf lib.types.path;
86 default = [ ];
87 description = ''
88 Packages providing extra SANE backends to enable.
89
90 ::: {.note}
91 The example contains the package for HP scanners, and the package for
92 Apple AirScan and Microsoft WSD support (supports many
93 vendors/devices).
94 :::
95 '';
96 example = lib.literalExpression "[ pkgs.hplipWithPlugin pkgs.sane-airscan ]";
97 };
98
99 hardware.sane.disabledDefaultBackends = lib.mkOption {
100 type = lib.types.listOf lib.types.str;
101 default = [ ];
102 example = [ "v4l" ];
103 description = ''
104 Names of backends which are enabled by default but should be disabled.
105 See `$SANE_CONFIG_DIR/dll.conf` for the list of possible names.
106 '';
107 };
108
109 hardware.sane.configDir = lib.mkOption {
110 type = lib.types.str;
111 internal = true;
112 description = "The value of SANE_CONFIG_DIR.";
113 };
114
115 hardware.sane.netConf = lib.mkOption {
116 type = lib.types.lines;
117 default = "";
118 example = "192.168.0.16";
119 description = ''
120 Network hosts that should be probed for remote scanners.
121 '';
122 };
123
124 hardware.sane.drivers.scanSnap.enable = lib.mkOption {
125 type = lib.types.bool;
126 default = false;
127 example = true;
128 description = ''
129 Whether to enable drivers for the Fujitsu ScanSnap scanners.
130
131 The driver files are unfree and extracted from the Windows driver image.
132 '';
133 };
134
135 hardware.sane.drivers.scanSnap.package = lib.mkPackageOption pkgs [ "sane-drivers" "epjitsu" ] {
136 extraDescription = ''
137 Useful if you want to extract the driver files yourself.
138
139 The process is described in the {file}`/etc/sane.d/epjitsu.conf` file in
140 the `sane-backends` package.
141 '';
142 };
143
144 hardware.sane.openFirewall = lib.mkOption {
145 type = lib.types.bool;
146 default = false;
147 description = ''
148 Open ports needed for discovery of scanners on the local network, e.g.
149 needed for Canon scanners (BJNP protocol).
150 '';
151 };
152
153 services.saned.enable = lib.mkOption {
154 type = lib.types.bool;
155 default = false;
156 description = ''
157 Enable saned network daemon for remote connection to scanners.
158
159 saned would be run from `scanner` user; to allow
160 access to hardware that doesn't have `scanner` group
161 you should add needed groups to this user.
162 '';
163 };
164
165 services.saned.extraConfig = lib.mkOption {
166 type = lib.types.lines;
167 default = "";
168 example = "192.168.0.0/24";
169 description = ''
170 Extra saned configuration lines.
171 '';
172 };
173
174 };
175
176 ###### implementation
177
178 config = lib.mkMerge [
179 (lib.mkIf enabled {
180 hardware.sane.configDir = lib.mkDefault "${saneConfig}/etc/sane.d";
181
182 environment.systemPackages = backends;
183 environment.sessionVariables = env;
184 environment.etc."sane-config".source = config.hardware.sane.configDir;
185 environment.etc."sane-libs".source = "${saneConfig}/lib/sane";
186 services.udev.packages = backends;
187 # sane sets up udev rules that tag scanners with `uaccess`. This way, physically logged in users
188 # can access them without belonging to the `scanner` group. However, the `scanner` user used by saned
189 # does not have a real logind seat, so `uaccess` is not enough.
190 services.udev.extraRules = ''
191 ENV{DEVNAME}!="", ENV{libsane_matched}=="yes", RUN+="${pkgs.acl}/bin/setfacl -m g:scanner:rw $env{DEVNAME}"
192 '';
193
194 users.groups.scanner.gid = config.ids.gids.scanner;
195 networking.firewall.allowedUDPPorts = lib.mkIf config.hardware.sane.openFirewall [ 8612 ];
196
197 systemd.tmpfiles.rules = [
198 "d /var/lock/sane 0770 root scanner - -"
199 ];
200 })
201
202 (lib.mkIf config.services.saned.enable {
203 networking.firewall.connectionTrackingModules = [ "sane" ];
204
205 systemd.services."saned@" = {
206 description = "Scanner Service";
207 environment = lib.mapAttrs (name: val: toString val) env;
208 serviceConfig = {
209 User = "scanner";
210 Group = "scanner";
211 ExecStart = "${pkg}/bin/saned";
212 };
213 };
214
215 systemd.sockets.saned = {
216 description = "saned incoming socket";
217 wantedBy = [ "sockets.target" ];
218 listenStreams = [
219 "0.0.0.0:6566"
220 "[::]:6566"
221 ];
222 socketConfig = {
223 # saned needs to distinguish between IPv4 and IPv6 to open matching data sockets.
224 BindIPv6Only = "ipv6-only";
225 Accept = true;
226 MaxConnections = 64;
227 };
228 };
229
230 users.users.scanner = {
231 uid = config.ids.uids.scanner;
232 group = "scanner";
233 extraGroups = [ "lp" ] ++ lib.optionals config.services.avahi.enable [ "avahi" ];
234 };
235 })
236 ];
237
238}