1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.xrdp;
7
8 confDir = pkgs.runCommand "xrdp.conf" { preferLocalBuild = true; } ''
9 mkdir -p $out
10
11 cp -r ${cfg.package}/etc/xrdp/* $out
12 chmod -R +w $out
13
14 cat > $out/startwm.sh <<EOF
15 #!/bin/sh
16 . /etc/profile
17 ${lib.optionalString cfg.audio.enable "${cfg.audio.package}/libexec/pulsaudio-xrdp-module/pulseaudio_xrdp_init"}
18 ${cfg.defaultWindowManager}
19 EOF
20 chmod +x $out/startwm.sh
21
22 substituteInPlace $out/xrdp.ini \
23 --replace "#rsakeys_ini=" "rsakeys_ini=/run/xrdp/rsakeys.ini" \
24 --replace "certificate=" "certificate=${cfg.sslCert}" \
25 --replace "key_file=" "key_file=${cfg.sslKey}" \
26 --replace LogFile=xrdp.log LogFile=/dev/null \
27 --replace EnableSyslog=true EnableSyslog=false
28
29 substituteInPlace $out/sesman.ini \
30 --replace LogFile=xrdp-sesman.log LogFile=/dev/null \
31 --replace EnableSyslog=1 EnableSyslog=0 \
32 --replace startwm.sh $out/startwm.sh \
33 --replace reconnectwm.sh $out/reconnectwm.sh \
34
35 # Ensure that clipboard works for non-ASCII characters
36 sed -i -e '/.*SessionVariables.*/ a\
37 LANG=${config.i18n.defaultLocale}\
38 LOCALE_ARCHIVE=${config.i18n.glibcLocales}/lib/locale/locale-archive
39 ' $out/sesman.ini
40
41 ${cfg.extraConfDirCommands}
42 '';
43in
44{
45
46 ###### interface
47
48 options = {
49
50 services.xrdp = {
51
52 enable = mkEnableOption "xrdp, the Remote Desktop Protocol server";
53
54 package = mkPackageOptionMD pkgs "xrdp" { };
55
56 audio = {
57 enable = mkEnableOption "audio support for xrdp sessions. So far it only works with PulseAudio sessions on the server side. No PipeWire support yet";
58 package = mkPackageOptionMD pkgs "pulseaudio-module-xrdp" {};
59 };
60
61 port = mkOption {
62 type = types.port;
63 default = 3389;
64 description = ''
65 Specifies on which port the xrdp daemon listens.
66 '';
67 };
68
69 openFirewall = mkOption {
70 default = false;
71 type = types.bool;
72 description = "Whether to open the firewall for the specified RDP port.";
73 };
74
75 sslKey = mkOption {
76 type = types.str;
77 default = "/etc/xrdp/key.pem";
78 example = "/path/to/your/key.pem";
79 description = ''
80 ssl private key path
81 A self-signed certificate will be generated if file not exists.
82 '';
83 };
84
85 sslCert = mkOption {
86 type = types.str;
87 default = "/etc/xrdp/cert.pem";
88 example = "/path/to/your/cert.pem";
89 description = ''
90 ssl certificate path
91 A self-signed certificate will be generated if file not exists.
92 '';
93 };
94
95 defaultWindowManager = mkOption {
96 type = types.str;
97 default = "xterm";
98 example = "xfce4-session";
99 description = ''
100 The script to run when user log in, usually a window manager, e.g. "icewm", "xfce4-session"
101 This is per-user overridable, if file ~/startwm.sh exists it will be used instead.
102 '';
103 };
104
105 confDir = mkOption {
106 type = types.path;
107 default = confDir;
108 internal = true;
109 description = ''
110 Configuration directory of xrdp and sesman.
111
112 Changes to this must be made through extraConfDirCommands.
113 '';
114 readOnly = true;
115 };
116
117 extraConfDirCommands = mkOption {
118 type = types.str;
119 default = "";
120 description = ''
121 Extra commands to run on the default confDir derivation.
122 '';
123 example = ''
124 substituteInPlace $out/sesman.ini \
125 --replace LogLevel=INFO LogLevel=DEBUG \
126 --replace LogFile=/dev/null LogFile=/var/log/xrdp.log
127 '';
128 };
129 };
130 };
131
132 ###### implementation
133
134 config = lib.mkMerge [
135 (mkIf cfg.audio.enable {
136 environment.systemPackages = [ cfg.audio.package ]; # needed for autostart
137
138 hardware.pulseaudio.extraModules = [ cfg.audio.package ];
139 })
140
141 (mkIf cfg.enable {
142
143 networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
144
145 # xrdp can run X11 program even if "services.xserver.enable = false"
146 xdg = {
147 autostart.enable = true;
148 menus.enable = true;
149 mime.enable = true;
150 icons.enable = true;
151 };
152
153 fonts.enableDefaultPackages = mkDefault true;
154
155 environment.etc."xrdp".source = "${confDir}/*";
156
157 systemd = {
158 services.xrdp = {
159 wantedBy = [ "multi-user.target" ];
160 after = [ "network.target" ];
161 description = "xrdp daemon";
162 requires = [ "xrdp-sesman.service" ];
163 preStart = ''
164 # prepare directory for unix sockets (the sockets will be owned by loggedinuser:xrdp)
165 mkdir -p /tmp/.xrdp || true
166 chown xrdp:xrdp /tmp/.xrdp
167 chmod 3777 /tmp/.xrdp
168
169 # generate a self-signed certificate
170 if [ ! -s ${cfg.sslCert} -o ! -s ${cfg.sslKey} ]; then
171 mkdir -p $(dirname ${cfg.sslCert}) || true
172 mkdir -p $(dirname ${cfg.sslKey}) || true
173 ${lib.getExe pkgs.openssl} req -x509 -newkey rsa:2048 -sha256 -nodes -days 365 \
174 -subj /C=US/ST=CA/L=Sunnyvale/O=xrdp/CN=www.xrdp.org \
175 -config ${cfg.package}/share/xrdp/openssl.conf \
176 -keyout ${cfg.sslKey} -out ${cfg.sslCert}
177 chown root:xrdp ${cfg.sslKey} ${cfg.sslCert}
178 chmod 440 ${cfg.sslKey} ${cfg.sslCert}
179 fi
180 if [ ! -s /run/xrdp/rsakeys.ini ]; then
181 mkdir -p /run/xrdp
182 ${pkgs.xrdp}/bin/xrdp-keygen xrdp /run/xrdp/rsakeys.ini
183 fi
184 '';
185 serviceConfig = {
186 User = "xrdp";
187 Group = "xrdp";
188 PermissionsStartOnly = true;
189 ExecStart = "${pkgs.xrdp}/bin/xrdp --nodaemon --port ${toString cfg.port} --config ${confDir}/xrdp.ini";
190 };
191 };
192
193 services.xrdp-sesman = {
194 wantedBy = [ "multi-user.target" ];
195 after = [ "network.target" ];
196 description = "xrdp session manager";
197 restartIfChanged = false; # do not restart on "nixos-rebuild switch". like "display-manager", it can have many interactive programs as children
198 serviceConfig = {
199 ExecStart = "${pkgs.xrdp}/bin/xrdp-sesman --nodaemon --config ${confDir}/sesman.ini";
200 ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID";
201 };
202 };
203
204 };
205
206 users.users.xrdp = {
207 description = "xrdp daemon user";
208 isSystemUser = true;
209 group = "xrdp";
210 };
211 users.groups.xrdp = {};
212
213 security.pam.services.xrdp-sesman = {
214 allowNullPassword = true;
215 startSession = true;
216 };
217
218 })
219 ];
220
221}