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