wallix / redemption

A GPL RDP proxy
GNU General Public License v2.0
213 stars 87 forks source link

How to use this project to capture video of an RDP session? #162

Open sdbbs opened 1 year ago

sdbbs commented 1 year ago

I have managed to successfully compile redemption on Ubuntu 20.04 ( see also https://unix.stackexchange.com/questions/734205/dh-install-cannot-find-files-tried-in-debian-buildtmp-but-files-exist-in-debi ), but now I realize, I have no idea how to use it.

What I'm interested in, is capturing my session with a remote PC as video. In the session, I run a couple of graphics/CPU intensive apps, so if I start a screen capture like OBS Studio along with remote desktop (VNC or RDP) everything grinds down to a halt. The remote PC is Windows 10, so RDP is behaving a lot more responsive (as far as I can see desktop decorations etc are removed) even when the intensive applications run, and this is why I want to try capturing the RDP session as video.

I have seen in the README that there are possibilities to capture an RDP session to a video, but I don't get how/what I have to set up, to get a connection to my target server, and capture that session as a video.

Yes, I have seen How to use this project · Issue #25 · wallix/redemption, and I'm still not getting it.

RTFM. Haven't you found README.md ?

Yes, I have, multiple times - still not getting it.

So, I can see there is a program rdpproxy. Just by the naming, I would have imagined, it would have had a local port (maybe 3389), and then I'd specify target address and port - and then I could use my preferred RDP software (remmina) to connect to the proxy, which would have then forwarded/proxied usernames/passwords and traffic between my remmina on my Ubuntu, and the target remote server; and while doing that, it could have captured something that could be converted to video.

However, that is not how things work; for one:

$ rdpproxy --help
Usage: rdpproxy [options]

  -h, --help                          produce help message
  -v, --version                       show software version
  -k, --kill[={on|off}]               shut down rdpproxy
  -n, --nodaemon[={on|off}]           don't fork into background
  -u, --uid=<uid>                     run with given uid
  -g, --gid=<gid>                     run with given gid
  -c, --check[={on|off}]              check installation files
  -f, --force[={on|off}]              remove application lock file
  -F, --ignore-lock-file[={on|off}]   ignore application lock file
  -N, --nofork[={on|off}]             not forkable (debug)
  --config-file=<path>                use an another ini file
  --print-spec                        Show file spec for rdpproxy.ini
  --print-rdp-cp-spec                 Show connection policy spec for rdp protocol
  --print-jhrdp-cp-spec               Show connection policy spec for jhrdp protocol
  --print-vnc-cp-spec                 Show connection policy spec for vnc protocol
  --print-cp-mapping                  Show connection policy mapping for sesman
  --print-default-ini                 Show default rdpproxy.ini

... rdpproxy has no command options for setting any ports.

So, the readme says:

Run ReDemPtion

To test it, executes:

$ python tools/passthrough/passthrough.py

# /usr/local/bin/rdpproxy -nf

Now, at that point you'll just have two servers waiting for connections not much fun. You still have to run some RDP client to connect to proxy. Choose whichever you like xfreerdp, rdesktop, remmina, tsclient on Linux or of course mstsc.exe if you are on windows. All are supposed to work. If some problem occurs just report it to us so that we can correct it.

Ok, I did that on the PC, say, 192.168.0.10; and then from 192.168.0.12 I run remmina, and intiate RDP connection to 192.168.0.10:3389; I get this:

image

Ok, I did not expect this; what makes this screen, passthrough.py or rdpproxy -nf?

In any case, one of those programs creates the screen - so what does that mean, that rdpproxy proxies 127.0.0.1 to ... 127.0.0.1?

Well, that is all fine as a starting example, however it should have been written in the README what should be expected of this example.

But then, good that I know how the example works now - but how do I use this for my use case? That is, how do I proxy 127.0.0.1 (current machine) to public IP of remote machine, as in example.com:3389? How can I specify a target port that is not 3389 but maybe due to port forwarding it is, say, 13389 (so, example.com:13389)? And how do I capture a video (or rather, as per README, a .mwrm or .wrm log file, which can thereafter be converted to video)?

If rdpproxy cannot be used directly for this purpose, are there other programs in this project that could be used instead?

sdbbs commented 1 year ago

Ok, I found also https://github.com/wallix/redemption/blob/future/docs/manual_to_redemption.rst - this explains a bit more:

Redemption is a versatile RDP proxy, meaning one will connect to remote desktops through Redemption. This allows to centralize remote connection creating a single end point for several desktops.

Running

For a local test, the usual options are -n and -f. The first option prevents Redemption from forking in the background, and the second makes sure no other instance is running. ::

$ sudo /usr/local/bin/rdpproxy -nf

And now what ? If everything went ok, you should be facing a waiting daemon ! You need two more things; first a client to connect to Redemption, second a server with RDP running (a Windows server, Windows XP Pro, etc.).

Redemption uses a hook file to get its target, username and password. This file is in utils/authhook.py and as you noticed it is written in Python. Two dictionnaries are important, the first one stores passwords, the second one stores the targets.

Ok, so I guess that blue screen above is part of the "allows to centralize remote connection creating a single end point for several desktops"

Unfortunately, as far as authhook.py goes - How to use this project · Issue #25 · wallix/redemption:

The part about authhook.py is out of date and has been replaced by passthrough.py. Currently, the internal bouncer module is not accessible without modification of passthrough.py and only the interactive login is used.

Also, I've found rdpclient:

$ rdpclient --help
Usage: rdpclient [options]

  -h, --help                          produce help message
  -v, --version                       show software version
  -t, --target-device=<address>
  -u, --username=<username>
  -p, --password=<password>
  -P, --port=<port>
  -a, --inactivity-time=<milliseconds>  milliseconds inactivity before sreenshot
  -m, --max-time=<milliseconds>       maximum milliseconds before sreenshot
  -s, --screen-output=<path>          png screenshot path
  -r, --record-path=<path>            dump socket path
  -V, --vnc[={on|off}]                enable vnc instead of rdp
  -l, --lcg[={on|off}]                use LCGRandom
  -b, --load-balance-info=<data>
  -n, --ini=<path>                    load ini filename
  -c, --cert-check=<number>           
    0 = fails if certificates doesn't match or miss.
    1 = fails if certificate doesn't match, succeed if no known certificate
    2 = succeed if certificates exists (not checked), fails if missing.
    3 = always succeed.
  --verbose=<verbosity>

This at least has something that looks like target address, and even has a record path; however:

$ rdpclient --target_device="127.0.0.1"
Bad option at parameter 1 (--target_device=127.0.0.1)

Why a "bad option"? How is "address" supposed to be formatted, then?

And how is one supposed to work with rdpclient?

jonathanpoelen commented 1 year ago

The configuration of rdpproxy is done from the ini file in /etc/rdpproxy/rdpproxy.ini which can be generated with rdpproxy --print-default-ini. redrec is also configured from this file, framerate and notimestamp may interest you.

For a connection to a target without going through the interactive module (the screen you see), you need to specify a password and a login of the form {username}@{ip} when connecting to the proxy. This is not done by remmina without having configured it (the example given with xfreerdp should work).

The proxy then sends this information to passthrough.py which responds with a list of variables to initialize a module with these options. To enable recording, you must uncomment the line kv[u'is_rec'] = u'1'.

rdpclient will check that a connection is possible, but you will not be able to do anything with it. There is theoretically projects/qtclient that would be suitable, but the recording doesn't work. With the work on an automatable client, it's not impossible that qtclient is taken over to serve as a helper and that the recording part will be reworked.

sdbbs commented 1 year ago

Many thanks for the response, @jonathanpoelen - much appreciated!

The configuration of rdpproxy is done from the ini file in /etc/rdpproxy/rdpproxy.ini which can be generated with rdpproxy --print-default-ini. redrec is also configured from this file, framerate and notimestamp may interest you.

Excellent, that is crucial info I was missing! I appreciate very much that this project provided the ./tools/packager.py, so I can build a .deb package; managed to also put all other executables in it (see unix.se:734205) - however now I realized, even after installing that .deb with dpkg -i, I did not get a /etc/rdpproxy/rdpproxy.ini. So I had to do:

sudo bash -c 'rdpproxy --print-default-ini > /etc/rdpproxy/rdpproxy.ini'

... to create this file; I will now look into how to configure it properly for my use case.

For a connection to a target without going through the interactive module (the screen you see), you need to specify a password and a login of the form {username}@{ip} when connecting to the proxy. This is not done by remmina without having configured it (the example given with xfreerdp should work).

Excellent, this is also crucal info! I will look into configuring remmina for the required for of the login - will probably have to test with xfreerdp while doing that, too.

The proxy then sends this information to passthrough.py which responds with a list of variables to initialize a module with these options. To enable recording, you must uncomment the line kv[u'is_rec'] = u'1'.

Awesome, this explains things a lot more for me! However, I'd like to clarify: does passthrough.py, after it initializes a module, get involved with (RDP) traffic, or recording, in any way? Or does that part (handling traffic and recording) get completely overtaken by rdpproxy once initialized?

Btw, great work from this project on conceptualizing a proxy that would be able to handle (and record) both VNC and RDP - having looked a bit into remote protocols, that is an idea I had thought for a while would be awesome, but also "too hard" to do - great to see this project brought the concept to implementation stage!

rdpclient will check that a connection is possible, but you will not be able to do anything with it.

Ok, I need some more clarification here. In the discussion so far, I've had the following model in my head:

So, in this model, if I use rdpclient as ... well ... and RDP client, then why would I "not be able to do anything with it"? If it just initializes the RDP conneciton, and shows the session in a window, and the recording is taken over by rdpproxy - well, that's all I would want to do with it, quite the opposite from "not being able to do anything with it"?! Unless I'm misunderstanding something in the mechanism of how things work here (which is also quite likely).

There is theoretically projects/qtclient that would be suitable, but the recording doesn't work. With the work on an automatable client, it's not impossible that qtclient is taken over to serve as a helper and that the recording part will be reworked.

Great, thanks for noting that - I also managed to find qtclient, and managed to compile it; and also experienced the recording does not work, so it's great to have that confirmed. I will jot down my experience with qtclient in the next post, as it was otherwise quite difficult to find any info on it.

sdbbs commented 1 year ago

Right - so, I realized qtclient exists, and I tried to compile it. On Ubuntu 20.04 I also needed these packages to complete the compilation:

sudo apt install qtbase5-dev qt5-default qt5-qmake qtcreator qtbase5-examples qtbase5-doc-html
sudo apt install libphonon4qt5-dev

The compilation otherwise completed with:

cd projects/qtclient
bjam qtclient

Now, the thing is, the Ubuntu machine I was trying this on was headless, so I wanted to also put the executable, qt5client (note the 5 in the name of the executable), in the .deb with the other redemption programs. This turned out to be quite gruelling, I did not have fun with bjam, but I managed somehow - see https://stackoverflow.com/questions/75340407/bjam-build-subfolder-project-change-directory-to-subfolder-project-before-bui

Anyways, at first, I just tried qt5client --help from a terminal, and among other text, I got these messages:

WARNING: bool Phonon::FactoryPrivate::createBackend() phonon backend plugin could not be loaded
WARNING: bool Phonon::FactoryPrivate::createBackend() phonon backend plugin could not be loaded
WARNING: bool Phonon::FactoryPrivate::createBackend() phonon backend plugin could not be loaded
WARNING: bool Phonon::FactoryPrivate::createBackend() phonon backend plugin could not be loaded
WARNING: Phonon::createPath: Cannot connect  Phonon::MediaObject ( no objectName ) to  Phonon::AudioOutput ( no objectName ).

After consulting https://askubuntu.com/questions/715587/phonon-backend-plugin-could-not-be-loaded, the solution for this was:

sudo apt install phonon4qt5 # not enough
sudo apt install phonon4qt5-backend-gstreamer phonon4qt5-backend-vlc

Right, so now, this is the result of qt5client --help for me:

$ qt5client --help

Client ReDemPtion Help menu.

  -h, --help                          Show help
  -v, --version                       Show version

========= Connection =========

  -u, --username=<value>              Set target session user name
  -p, --password=<value>              Set target session user password
  -i, --ip=<value>                    Set target IP address
  -P, --port=<value>                  Set port to use on target

========= Verbose =========

  --rdpdr[={on|off}]                  Active rdpdr logs
  --rdpsnd[={on|off}]                 Active rdpsnd logs
  --cliprdr[={on|off}]                Active cliprdr logs
  --graphics[={on|off}]               Active graphics logs
  --printer[={on|off}]                Active printer logs
  --rdpdr-dump[={on|off}]             Actives rdpdr logs and dump brute rdpdr PDU
  --cliprd-dump[={on|off}]            Actives cliprdr logs and dump brute cliprdr PDU
  --basic-trace[={on|off}]            Active basic-trace logs
  --connection[={on|off}]             Active connection logs
  --rail-order[={on|off}]             Active rail-order logs
  --asynchronous-task[={on|off}]      Active asynchronous-task logs
  --capabilities[={on|off}]           Active capabilities logs
  --rail[={on|off}]                   Active rail logs
  --rail-dump[={on|off}]              Actives rail logs and dump brute rail PDU

========= Protocol =========

  --vnc                               Set connection mod to VNC
  --rdp                               Set connection mod to RDP (default).
  --remote-app[={on|off}]             Connection as remote application.
  --remote-exe=command                Connection as remote application and set the line command.
  --span[={on|off}]                   Span the screen size on local screen
  --enable-clipboard[={on|off}]       Enable clipboard sharing
  --enable-nla[={on|off}]             Enable NLA protocol
  --enable-tls[={on|off}]             Enable TLS protocol
  --tls-min-level=<value>             Minimal TLS protocol level
  --tls-max-level=<value>             Maximal TLS protocol level allowed
  --show_common_cipher_list[={on|off}]  Show TLS Cipher List
  --cipher_string=<value>             Set TLS Cipher allowed for TLS <= 1.2
  --enable-sound[={on|off}]           Enable sound
  --enable-fullwindowdrag[={on|off}]  Enable full window draging
  --enable-menuanimations[={on|off}]  Enable menu animations
  --enable-theming[={on|off}]         Enable theming
  --enable-cursor-shadow[={on|off}]   Enable cursor shadow
  --enable-cursorsettings[={on|off}]  Enable cursor settings
  --enable-font-smoothing[={on|off}]  Enable font smoothing
  --enable-desktop-composition[={on|off}]  Enable desktop composition
  --vnc-applekeyboard[={on|off}]      Set keyboard compatibility mod with apple VNC server
  --keep_alive_frequence=<value>      Set timeout to send keypress to keep the session alive
  --remotefx[={on|off}]               enable remotefx

========= Client =========

  --width=<value>                     Set screen width
  --height=<value>                    Set screen height
  --bpp=bit_per_pixel                 Set bit per pixel (8, 15, 16, 24, 32)
  --keylayout=<value>                 Set windows keylayout
  --enable-record[={on|off}]          Enable session recording as .wrm movie
  --persist                           Set connection to persist
  --timeout=time                      Set timeout response before to disconnect in millisecond
  --share-dir=directory               Set directory path on local disk to share with your session.
  --remote-dir=directory              Remote working directory
WARNING (2809660/2809660) -- readdir error: (1504) Exception ERR_TRANSPORT_READ_FAILED no: 1504
QObject::connect: No such slot QtOptions::deletePressed() in src/qt_input_output_api/qt_graphics_components/qt_options_window.hpp:241
QObject::connect: No such slot QtOptions::deletePressed() in src/qt_input_output_api/qt_graphics_components/qt_options_window.hpp:241
free(): invalid size
Aborted (core dumped)

Notice that it crashes/segfaults in the end (free(): invalid size, Aborted (core dumped)); this was worrying, I thought it was possibly because of the "QObject::connect: No such slot QtOptions::deletePressed()" messages. However, then I just decided to try it like this:

$ qt5client --width=1366 --height=768
WARNING (2809944/2809944) -- readdir error: (1504) Exception ERR_TRANSPORT_READ_FAILED no: 1504
QObject::connect: No such slot QtOptions::deletePressed() in src/qt_input_output_api/qt_graphics_components/qt_options_window.hpp:241
QObject::connect: No such slot QtOptions::deletePressed() in src/qt_input_output_api/qt_graphics_components/qt_options_window.hpp:241

... and in spite of those messages, I still get a GUI window! Nice - here is a screenshot:

redemption_qt5client

Some things I have noted:

Otherwise, qt5client worked fine for me in connecting to a remote server and providing me a window of the session; the only glitch I noticed that it registers mouse right-click in Windows Terminal with selected text (which makes a copy and removes selection), but when you right-click again to paste text, the text does not get pasted.

However, I also wanted to try recording, so I tried first:

qt5client --width=1366 --height=768 --enable-record=on

I made an RDP session with a remote server here, and after a while disconnected, but I could not find any new capture files that got generated.

Then I realized I have to click Options/General/Record Movie as on the screenshot; then again, I made an RDP session with a remote server, and after a while disconnected - and I looked up anything that looked like a capture file; I found them here:

$ ls -la $HOME/src/redemption.git/projects/qtclient/DATA/replay
total 13380
drwxrwxr-x 2 USER USER     4096 Feb  4 04:51  .
drwxrwxr-x 6 USER USER     4096 Feb  4 04:29  ..
-rwxrwxrwx 1 USER USER  6706523 Feb  4 05:29 'Sat Feb  4 05:29:14 2023-Replay-000000.wrmred-l8RfuB.tmp'
-rwxrwxrwx 1 USER USER       25 Feb  4 05:29 'Sat Feb  4 05:29:14 2023-Replay.mwrmred-QZCX5z.tmp'

Well, both the location and the naming format was kind of unexpected for me; also, I cannot tell why there are two files, where one has *mwrm* in the name and is only 25 bytes, and the other has *wrm* in the name and is many bytes ...

Anyways, so I tried to convert these to redrec, as per the README:

$ redrec --encryption=enable -f --video-codec mp4 -i $HOME/src/redemption_git/projects/qtclient/DATA/replay/'Sat Feb  4 05:29:14 2023-Replay.mwrmred-QZCX5z.tmp' -o $PWD/test.mp4
Output file is "$HOME/Desktop/test.mp4".
wrm file not found in mwrm file

$ redrec --encryption=enable -f --video-codec mp4 -i $HOME/src/redemption.git/projects/qtclient/DATA/replay/'Sat Feb  4 05:29:14 2023-Replay-000000.wrmred-l8RfuB.tmp' -o $PWD/test.mp4
Output file is "$HOME/Desktop/test.mp4".
decrypt failed: with id=1504 ERR_TRANSPORT_READ_FAILED

There are no new files generated with either of these commands; I guess this confirms that recording with qtclient does not yet work.

Yup, I think that was about it for notes about first-time qtclient use; in all, its pretty cool most of its functionality works fine, it's just the recording that is a show stopper for me, otherwise it's only smaller UI glitches that I see as problems; overall, pretty good!

jonathanpoelen commented 1 year ago

Among the executables, only rdpproxy, redrec and rdpinichecker are really useful. The others are for testing purposes.

rdpproxy.ini is not installed because it is not needed to run the proxy and it generates conflicts when updating the package. The --print-default-ini option is mainly used to find out the default variables and values used by the proxy. You could simply have a file that contains only the values you are interested in. By the way, you can use the --config-file option if you want multiple configurations or have an ini file in a different location.

I know that the README gives rather little information and some of the docs are outdated (this is clearly visible with last issues where I explain things that should be in it), so we should take some time to complete this.

Awesome, this explains things a lot more for me! However, I'd like to clarify: does passthrough.py, after it initializes a module, get involved with (RDP) traffic, or recording, in any way? Or does that part (handling traffic and recording) get completely overtaken by rdpproxy once initialized?

All the recording part will be done by the proxy, passthrough.py is used to provide configurations and target information, but does not handle RDP or VNC traffic. It has options like closing session, but passthrough.py does not send them. passthrough.py is basic, the idea is that people make their own version for their own needs (a note somewhere indicates where to find the exchanged values).

Ok, I need some more clarification here. In the discussion so far, I've had the following model in my head: I start up passthough.py, it keeps running listening to a file socket so it can talk to rdpproxy

Yes

I start up rdpproxy - it first reads through /etc/rdpproxy/rdpproxy.ini to get configured (including any info for target remote RDP server, and recording), then starts listening on a local IP port on that machine for incomming RDP connections

Yes. The RDP server information is mostly protocol configuration, most of the ini options can also be sent via passthrough.py if target specific configurations are needed.

Then I'd start up an RDP client (remmina, xfreerdp) and point it to the IP and port of the machine where rdpproxy is listening; upon connection, rdpproxy asks passthrough.py for initialization, gets initialized, and from that point on, passes the RDP username/password credentials to the target remote server; once authorized, the session with the target remote server starts, and is reproduced interactively in the RDP client window (as well as being recorded by rdpproxy if so configured).

Yes

So, in this model, if I use rdpclient as ... well ... and RDP client, then why would I "not be able to do anything with it"?

Your model assumes that clients necessarily display a window, rdpclient does not. The name is misleading, but in our vision of a world without direct rendering, it was natural :p. It will be removed soon because our new test tool will do the same thing and more (with a much less misleading name).

Right - so, I realized qtclient exists, and I tried to compile it. On Ubuntu 20.04 I also needed these packages to complete the compilation: sudo apt install qtbase5-dev qt5-default qt5-qmake qtcreator qtbase5-examples qtbase5-doc-html sudo apt install libphonon4qt5-dev

I think there is too much dependency and that qtbase5-dev is enough. qt5-qmake is not needed since we don't use qmake, qtcreator is an IDE, qtbase5-examples are code examples and qtbase5-doc-html is just doc.

libphonon4qt5-dev is what handles the sound part, the warnings on that are not important if you don't need the sound through RDP. Note on that, the sound is not in the recordings, but there must be a way at system level to record the sound of an application. qtclient could do this recording, but it would be a separate file that would have to be merged with the final video (it's easy with ffmpeg in cli).

To have qtclient in one package, I think it's best to make another one on the side. But if you want, you can add cd projects/qtclient; bjam qtclient / cd projects/qtclient; bjam install in packaging/template/debian/rules, add the dependencies in the packaging/targets/ files and the new executable in packaging/template/debian/redemption.install (the last step may be useless thanks to %PREFIX%/bin/*).

For .tmp files, this is a sign that the recording is still in progress (I think it is not stopped at the time of disconnection). It's normally recoverable by renaming the files and manually writing the mwrm, but I end up with an incomplete video. I'm thinking of doing a quick fix next week about stopping the recording, but if it's not enough, it will wait.

sdbbs commented 1 year ago

Many thanks again, @jonathanpoelen - excellent information!

Btw, I got a working setup with recording, and I will write about that in the next post.

Among the executables, only rdpproxy, redrec and rdpinichecker are really useful. The others are for testing purposes.

Got it, good to know!

rdpproxy.ini is not installed because it is not needed to run the proxy and it generates conflicts when updating the package.

Thanks, good to know - especially since at first I was suspecting I did something wrong when building the .deb file.

You could simply have a file that contains only the values you are interested in.

I appreciate that functionality - good to know it is there!

All the recording part will be done by the proxy, passthrough.py is used to provide configurations and target information, but does not handle RDP or VNC traffic. It has options like closing session, but passthrough.py does not send them. passthrough.py is basic, the idea is that people make their own version for their own needs (a note somewhere indicates where to find the exchanged values).

Excellent - thanks for confirming that, good to know that I wasn't way too off in understanding how things generally work. I also ended up "independently confirming" (before reading your answer) that "the idea is that people make their own version for their own needs", in that in my working procedure, I had to hack passthrough.py to get things to work.

Also thanks for reviewing my model - I have just recently learned that Github supports Mermaid diagrams, so I've drawn my understanding of the proxying process, in my use context: the RDP client I use on my machine, communicates with Redemption (rdpproxy + passthrough.py) on another machine on the same local network, to proxy the traffic to an RDP server (here, a port forward of a Windows 10 PC with Remote Desktop enabled):

%%{init: {'theme':'base'}}%%
flowchart LR

    subgraph Border
        direction LR

        subgraph PC1["<div style='text-align: center;'>PC 1<br/><code>192.168.RDP.CLIENT</code></div>"]
            direction LR
            IP1["#nbsp;"]:::nodisplaypad;
            RdpClient["RDP Client<br/>(<code>remmina</code>, <code>xfreerdp</code> ...)"]:::wht;
        end

        subgraph PC2["<div style='text-align: center;'>PC 2</div>"]
            direction TB
            %%IP2["192.168.RDP.PROXY"]:::blank;
            subgraph PC2Contents["<div style='text-align: center;'><code>192.168.RDP.PROXY</code></div>"]
                direction LR
                RdpProxyPort("<code>port 3389</code>"):::port;
                RdpProxyA["<code><b>rdpproxy -nf</b></code>"]:::wht;
                RdpProxyPort <==> RdpProxyA;
                FileSocket("File socket<br/><code>/tmp/redemption-sesman-sock</code>"):::wht;
                RdpProxyB["<code><b>passthrough.py</b></code>"]:::wht;
                RdpProxyA <--> FileSocket;
                FileSocket <--> RdpProxyB;
            end
        end

        RdpClient <==> RdpProxyPort;

        subgraph CloudContainer[ ]
            direction LR
            %% [How to add a new shape? · Issue #1500 · mermaid-js/mermaid](https://github.com/mermaid-js/mermaid/issues/1500)
            CloudInternetNode("<div style='position:absolute;left:-0px;top:-8px;color:#35;bbb;font-size:120px;'>#9729;#65039;</div><div style='position:relative;z-index:100;color:black;padding:18px;'>#nbsp;<br/>#nbsp;#nbsp;#nbsp;#nbsp;Internet#nbsp;#nbsp;#nbsp;<br/>#nbsp;</div>");
        end

        RdpProxyA <==> CloudInternetNode;

        subgraph PC3["<div style='text-align: center;'>PC 3</div>"]
            direction TB
            %%IP2["192.168.RDP.PROXY"]:::blank;
            subgraph PC3Contents["<div style='text-align: center;'><code>209.85.RDP.SERVER</code></div>"]
                direction TB
                RdpServer["RDP Server<br/>(Target Windows PC)"]:::blank;
                RdpServerPort("<code>port 13389</code>"):::port;
                %%RdpServer <--> RdpServerPort; %% https://github.com/mermaid-js/mermaid/issues/2509
            end
        end

        CloudInternetNode <==> RdpServerPort;

    end

%% Defining Class Styles
%% background-image:"" passes, but not with any text inside quotes
%% node that classes here get defined with "#graph-div " prepended!
%% however, seemingly <img> and such are included via foreignObject, so cannot refer to these classes 
classDef BorderClass fill:#fff,stroke:#fff,stroke-width:4px,color:#fff,stroke-dasharray: 0 4,margin:0,padding:0;
classDef ContainerClass position:relative,padding:0,margin:0,fill:#fff,color:#000,stroke:none;
classDef ImgNodeClass fill:#fff,padding:0,margin:0,stroke:none,margin:0;
classDef wht fill:#fff,color:black,stroke:#000;
classDef blank fill:#fff,color:none,stroke:none;
classDef nodisplaypad display:none;
classDef port color:#500,fill:#fff,stroke:#c80,stroke-width:2px,font-weight:normal;

%% Custom Styles

%% Assigning Nodes to Classes
%% class gets applied to all childer, too!
class Border BorderClass;
class CloudContainer ContainerClass;
class CloudInternetNode ImgNodeClass;
class PC1,PC2,PC3 wht;
class PC2Contents,PC3Contents blank;

Your model assumes that clients necessarily display a window, rdpclient does not. The name is misleading,

Ah, that explains things! Indeed, it seems there is a naming conflict there ... although I didn't even get to thinking about rdpclient displaying a window, I got confused already when it refused to parse the --target_device command line argument :) ... and indeed, in my working setup, I use remmina to display the window (as on the diagram above).

... but in our vision of a world without direct rendering, it was natural :p. It will be removed soon because our new test tool will do the same thing and more (with a much less misleading name).

Please, PLEASE do not abandon your vision of a world without direct rendering!!!! In the setup I got working, (rdpproxy + passthrough.py) might as well run on a headless machine! So please leave an option for headless testing in the coming rdpclient replacement ...

I think there is too much dependency and that qtbase5-dev is enough. qt5-qmake is not needed since we don't use qmake, qtcreator is an IDE, qtbase5-examples are code examples and qtbase5-doc-html is just doc.

Awesome, great to know this - I just copy-pasted those lines from a tutorial somewhere, did not even get to thinking if those are too many packages.

libphonon4qt5-dev is what handles the sound part, the warnings on that are not important if you don't need the sound through RDP. Note on that, the sound is not in the recordings, but there must be a way at system level to record the sound of an application. qtclient could do this recording, but it would be a separate file that would have to be merged with the final video (it's easy with ffmpeg in cli).

Good to know - I do not use sound in my recording, but love that the option is there - and so I'm glad I compiled with libphonon4qt5-dev.

To have qtclient in one package, I think it's best to make another one on the side.

I would have gladly done so, but looking at the ./tools/packager.py script, I couldn't figure out if it supported building of a qtclient package. Does an option for building a, say, redemption-qtclient.deb package easily exist currently?

But if you want, you can add cd projects/qtclient; bjam qtclient / cd projects/qtclient; bjam install in packaging/template/debian/rules, add the dependencies in the packaging/targets/ files and the new executable in packaging/template/debian/redemption.install (the last step may be useless thanks to %PREFIX%/bin/*).

Great, thanks for this. It turns out, the procedure I've described in https://stackoverflow.com/questions/75340407/bjam-build-subfolder-project-change-directory-to-subfolder-project-before-bui, while it handles qt5client correctly, it messes up paths for redrec so it cannot run anymore; I've updated my answer in that post with your instructions.

For .tmp files, this is a sign that the recording is still in progress (I think it is not stopped at the time of disconnection). It's normally recoverable by renaming the files and manually writing the mwrm, but I end up with an incomplete video. I'm thinking of doing a quick fix next week about stopping the recording, but if it's not enough, it will wait.

Good to know - now that I can confirm recording with rdpproxy -nf (and passthrough.py) works(forme), I don't really need to use qt5client, so I'm fine with waiting for the recording fix for qt5client :) Also, in my working setup, I can confirm that whatever .tmp files are captured, are in the end converted to (m)wrm files.

Thanks again for the feedback - and for the great software!

sdbbs commented 1 year ago

Right, so I managed to get recording with rdpproxy going; again, this is the diagram of the use context - the RDP client I use on my machine (remmina, communicates with Redemption (rdpproxy + passthrough.py) on another machine on the same local network, to proxy the traffic to an RDP server (here, a port forward of a Windows 10 PC with Remote Desktop enabled):

%%{init: {'theme':'base'}}%%
flowchart LR

    subgraph Border
        direction LR

        subgraph PC1["<div style='text-align: center;'>PC 1<br/><code>192.168.RDP.CLIENT</code></div>"]
            direction LR
            IP1["#nbsp;"]:::nodisplaypad;
            RdpClient["RDP Client<br/>(<code>remmina</code>, <code>xfreerdp</code> ...)"]:::wht;
        end

        subgraph PC2["<div style='text-align: center;'>PC 2</div>"]
            direction TB
            %%IP2["192.168.RDP.PROXY"]:::blank;
            subgraph PC2Contents["<div style='text-align: center;'><code>192.168.RDP.PROXY</code></div>"]
                direction LR
                RdpProxyPort("<code>port 3389</code>"):::port;
                RdpProxyA["<code><b>rdpproxy -nf</b></code>"]:::wht;
                RdpProxyPort <==> RdpProxyA;
                FileSocket("File socket<br/><code>/tmp/redemption-sesman-sock</code>"):::wht;
                RdpProxyB["<code><b>passthrough.py</b></code>"]:::wht;
                RdpProxyA <--> FileSocket;
                FileSocket <--> RdpProxyB;
            end
        end

        RdpClient <==> RdpProxyPort;

        subgraph CloudContainer[ ]
            direction LR
            %% [How to add a new shape? · Issue #1500 · mermaid-js/mermaid](https://github.com/mermaid-js/mermaid/issues/1500)
            CloudInternetNode("<div style='position:absolute;left:-0px;top:-8px;color:#35;bbb;font-size:120px;'>#9729;#65039;</div><div style='position:relative;z-index:100;color:black;padding:18px;'>#nbsp;<br/>#nbsp;#nbsp;#nbsp;#nbsp;Internet#nbsp;#nbsp;#nbsp;<br/>#nbsp;</div>");
        end

        RdpProxyA <==> CloudInternetNode;

        subgraph PC3["<div style='text-align: center;'>PC 3</div>"]
            direction TB
            %%IP2["192.168.RDP.PROXY"]:::blank;
            subgraph PC3Contents["<div style='text-align: center;'><code>209.85.RDP.SERVER</code></div>"]
                direction TB
                RdpServer["RDP Server<br/>(Target Windows PC)"]:::blank;
                RdpServerPort("<code>port 13389</code>"):::port;
                %%RdpServer <--> RdpServerPort; %% https://github.com/mermaid-js/mermaid/issues/2509
            end
        end

        CloudInternetNode <==> RdpServerPort;

    end

%% Defining Class Styles
%% background-image:"" passes, but not with any text inside quotes
%% node that classes here get defined with "#graph-div " prepended!
%% however, seemingly <img> and such are included via foreignObject, so cannot refer to these classes 
classDef BorderClass fill:#fff,stroke:#fff,stroke-width:4px,color:#fff,stroke-dasharray: 0 4,margin:0,padding:0;
classDef ContainerClass position:relative,padding:0,margin:0,fill:#fff,color:#000,stroke:none;
classDef ImgNodeClass fill:#fff,padding:0,margin:0,stroke:none,margin:0;
classDef wht fill:#fff,color:black,stroke:#000;
classDef blank fill:#fff,color:none,stroke:none;
classDef nodisplaypad display:none;
classDef port color:#500,fill:#fff,stroke:#c80,stroke-width:2px,font-weight:normal;

%% Custom Styles

%% Assigning Nodes to Classes
%% class gets applied to all childer, too!
class Border BorderClass;
class CloudContainer ContainerClass;
class CloudInternetNode ImgNodeClass;
class PC1,PC2,PC3 wht;
class PC2Contents,PC3Contents blank;

A redemption package was built from git on PC 2 (192.168.RDP.PROXY) with:

./tools/packager.py --build-package --force-build

... and installed with:

sudo dpkg -i ../redemption_10.4.41+focal_amd64.deb

... and /etc/rdpproxy/rdpproxy.ini was generated with:

sudo bash -c 'rdpproxy --print-default-ini > /etc/rdpproxy/rdpproxy.ini'

At this point, I reviewed rdpproxy.ini, and was surprised that I did not find target server settings there either (as there weren't any in the options listed by rdpproxy --help). I expected I should be able to specify target server settings in a file somewhere - and It turns out, I had to hack passthrough.py to do that.

However, there are still important settings in rdpproxy.ini - and one that really bit me was the setting that actually activates recording; even after I setup a working connection, I couldn't get any recording, and the log kept saying "Front::can_be_start_capture: Capture is not necessary". So I had to dig through the source to find src/front/front.hpp - and:

    bool is_capture_necessary()
    {
        return (this->ini.get<cfg::video::allow_rt_without_recording>()
            || this->ini.get<cfg::globals::is_rec>()
            || !bool(this->ini.get<cfg::video::disable_keyboard_log>() & KeyboardLogFlags::syslog)
            || ::contains_kbd_or_ocr_pattern(this->ini.get<cfg::context::pattern_kill>())
            || ::contains_kbd_or_ocr_pattern(this->ini.get<cfg::context::pattern_notify>()));
    }

At first, I found allow_rt_without_recording confusing (as its help says "Allow real-time view (4 eyes) without session recording enabled in the authorization", and I wanted to enable recording, not "allow ... view ... without session recording ..."), and I thought I should enable is_rec in the rdpproxy.ini config file instead - the problem being, that rdpproxy.ini did not contain any reference to is_rec. So I added it manually, but that did not make any difference (so probably is_rec is just a runtime parameter, but one stored in cfg::...). Then I set allow_rt_without_recording - and finally recording started!

That being said, here is what I changed in the rdpproxy.ini as a diff - some lines I did not change, but I commented them anyways as I thought they were interesting:

--- rdpproxy.original.ini   2023-02-06 06:41:51.910624593 +0100
+++ /etc/rdpproxy/rdpproxy.ini  2023-02-06 06:59:32.045115305 +0100
@@ -53,14 +53,14 @@
 #_advanced
 #close_timeout = 600

-# Session record options.
+# Session record options. # note
 # min = 0, max = 2
 #   0: No encryption (faster).
 #   1: No encryption, with checksum.
 #   2: Encryption enabled.
 # When session records are encrypted, they can be read only by the WAB where they have been generated.
 #_advanced
-#trace_type = 1
+#trace_type = 1 # note

 #_advanced
 #listen_address = 0.0.0.0
@@ -152,6 +152,10 @@
 #_hidden
 #minimal_memory_available_before_connection_silently_closed = 100

+# is_rec - NOT originally present in output from sudo bash -c 'rdpproxy --print-default-ini > /etc/rdpproxy/rdpproxy.ini'
+# apparently to enable recording of video
+#is_rec = 1 # apparently does not get registered, try allow_rt_without_recording instead
+
 [client]

 # bg-BG, bg-BG.latin, bs-Cy, cs-CZ, cs-CZ.programmers, cs-CZ.qwerty, cy-GB, da-DK, de-CH, de-DE, de-DE.ibm, el-GR, el-GR.220, el-GR.220_latin, el-GR.319, el-GR.319_latin, el-GR.latin, el-GR.polytonic, en-CA.fr, en-CA.multilingual, en-GB, en-IE, en-IE.irish, en-US, en-US.dvorak, en-US.dvorak_left, en-US.dvorak_right, en-US.international, es-ES, es-ES.variation, es-MX, et-EE, fi-FI.finnish, fo-FO, fr-BE, fr-BE.fr, fr-CA, fr-CH, fr-FR, hr-HR, hu-HU, is-IS, it-IT, it-IT.142, iu-La, kk-KZ, ky-KG, lb-LU, lt-LT, lt-LT.ibm, lv-LV, lv-LV.qwerty, mi-NZ, mk-MK, mn-MN, mt-MT.47, mt-MT.48, nb-NO, nl-BE, nl-NL, pl-PL, pl-PL.programmers, pt-BR.abnt, pt-BR.abnt2, pt-PT, ro-RO, ru-RU, ru-RU.typewriter, se-NO, se-NO.ext_norway, se-SE, se-SE, se-SE.ext_finland_sweden, sk-SK, sk-SK.qwerty, sl-SI, sr-Cy, sr-La, sv-SE, tr-TR.f, tr-TR.q, tt-RU, uk-UA, uz-Cy
@@ -1029,7 +1033,7 @@

 [video]

-# Specifies the type of data to be captured:
+# Specifies the type of data to be captured: # note
 # min = 0, max = 15
 #   0x00: none
 #   0x01: png
@@ -1038,17 +1042,17 @@
 # Note: values can be added (enable all: 0x01 + 0x02 + 0x08 = 0x0b)
 #_advanced
 #_hex
-#capture_flags = 11
+#capture_flags = 11 # note - default OK

-# Frame interval.
-# (in 1/10 seconds)
+# Frame interval. # note
+# (in 1/10 seconds) # note - max is 10 FPS
 #_advanced
-#png_interval = 10
+#png_interval = 10 # note

-# Time between 2 wrm movies.
+# Time between 2 wrm movies. # note
 # (in seconds)
 #_advanced
-#break_interval = 600
+#break_interval = 600 # note

 # Number of png captures to keep.
 # min = 0
@@ -1058,14 +1062,17 @@
 # maxlen = 4096
 #_hidden
 #hash_path = /var/rdpproxy/hash
+hash_path = /path/to/extern_disk/rdpproxy/hash

 # maxlen = 4096
 #_hidden
 #record_tmp_path = /var/rdpproxy/tmp
+record_tmp_path = /path/to/extern_disk/rdpproxy/tmp

 # maxlen = 4096
 #_hidden
 #record_path = /var/rdpproxy/recorded/rdp
+record_path = /path/to/extern_disk/rdpproxy/recorded

 # Disable keyboard log:
 # (Please see also "Keyboard input masking level" in "session_log".)
@@ -1101,20 +1108,20 @@
 #_hex
 #disable_file_system_log = 1

-# The method by which the proxy RDP establishes criteria on which to chosse a color depth for native video capture:
+# The method by which the proxy RDP establishes criteria on which to chosse a color depth for native video capture: # note
 # min = 0, max = 1
 #   0: 24-bit
 #   1: 16-bit
 #_advanced
-#wrm_color_depth_selection_strategy = 1
+#wrm_color_depth_selection_strategy = 1 # maybe need to make this 24-bit?

-# The compression method of native video capture:
+# The compression method of native video capture: # note
 # min = 0, max = 2
 #   0: no compression
 #   1: gzip
 #   2: snappy
 #_advanced
-#wrm_compression_algorithm = 1
+#wrm_compression_algorithm = 1 # note: default 1: gzip, ok

 # Needed to play a video with old ffplay or VLC v1.
 # Note: Useless with mpv, MPlayer or VLC v2.
@@ -1123,23 +1130,24 @@
 #_display_name=Bogus VLC frame rate
 #bogus_vlc_frame_rate = 1

-#_advanced
-#codec_id = mp4
+#_advanced # note
+#codec_id = mp4 # note

 # min = 1, max = 120
 # min = 0
 #_advanced
 #_display_name=Frame rate
 #framerate = 5
+framerate = 25

-# FFmpeg options for video codec. See https://trac.ffmpeg.org/wiki/Encode/H.264
+# FFmpeg options for video codec. See https://trac.ffmpeg.org/wiki/Encode/H.264 # note
 # /!\ Some browsers and video decoders don't support crf=0
 #_advanced
-#ffmpeg_options = crf=35 preset=superfast
+#ffmpeg_options = crf=35 preset=superfast # note

 # value: 0 or 1
 #_advanced
-#notimestamp = 0
+#notimestamp = 0 # refers to the timestamp in upper left corner of video

 # min = 0, max = 2
 #   0: Disabled. When replaying the session video, the content of the RDP viewer matches the size of the client's desktop
@@ -1151,16 +1159,19 @@
 # value: 0 or 1
 #_advanced
 #play_video_with_corrupted_bitmap = 0
+play_video_with_corrupted_bitmap = 1

 # Allow real-time view (4 eyes) without session recording enabled in the authorization
 # value: 0 or 1
 #allow_rt_without_recording = 0
+allow_rt_without_recording = 1

 # Allow to control permissions on recorded files with octal number
 # (is in octal or symbolic mode format (as chmod Linux command))
 # max = 777, min = 0
 #_hidden
 #file_permissions = 440
+file_permissions = 644

 [audit]

So, it seems that by default, these settings have break_interval = 600 which is "Time between 2 wrm movies", which as I understanding, means that the video capture will happen in 10 minute chunks (probably to allow for long captures, so you only lose the last 10 minutes if you run out of space on disk).

The notimestamp refers to this graphical timestamp added to upper left corner of the video (the cool thing is, you can leave notimestamp = 0 in the ini file during rdpproxy capture, and then change it to notimestamp = 1 during the redrec video conversion phase, and the timestamp will be gone from the final video):

timestamp

Anyways, now I needed to enter the target server credentials somewhere; and that ended up being in passthrough.py: a subclass MyAuthentifierSharedData(AuthentifierSharedData) was implemented with a method to (re)enforce the desired target settings, and changes to ACLPassthrough.start() were made so that this method is called after the initial "RDP blue screen" exchange, and to handle assignment of target_port - here is a diff:

diff --git a/tools/passthrough/passthrough.py b/tools/passthrough/passthrough.py
index d276754ca..3c6043a43 100755
--- a/tools/passthrough/passthrough.py
+++ b/tools/passthrough/passthrough.py
@@ -192,11 +192,29 @@ class AuthentifierSharedData():
         return self.shared.get(key) == MAGICASK

+class MyAuthentifierSharedData(AuthentifierSharedData):
+    def __init__(self, conn):
+        # init parent class
+        super(MyAuthentifierSharedData, self).__init__(conn)
+        self.target ettings_enforce_hardcoded()
+
+    def target_settings_enforce_hardcoded(self):
+        # override settings
+        self.shared[u'target_device'] = "209.85.RDP.SERVER.com_13389" # treating it as just a label
+        self.shared[u'target_host'] = "209.85.RDP.SERVER.com"
+        self.shared[u'target_port'] = "13389"
+        self.shared[u'target_login'] = "TARGET_USERNAME"
+        self.shared[u'target_password'] = "TARGET_PASSWORD"
+        #self.shared[u'login'] # maybe for the internal blue starting RDP screen shown on RDP client?
+        #self.shared[u'ip_client'] # filled automatically, this is the IP of the RDP client that connected to the rdpproxy that talks to us
+
+
 class ACLPassthrough():
     def __init__(self, conn, addr):
         self.proxy_conx = conn
         self.addr       = addr
-        self.shared = AuthentifierSharedData(conn)
+        #self.shared = AuthentifierSharedData(conn)
+        self.shared = MyAuthentifierSharedData(conn)

     def interactive_target(self, data_to_send):
         data_to_send.update({ u'module' : u'interactive_target' })
@@ -239,8 +257,18 @@ class ACLPassthrough():

     def start(self):
+        # NOTE: this start() method, runs only AFTER RDP blue screen has appeared on the RDP Client end,
+        # and the user has clicked to log in!
+        # NOTE: self.shared.receive_data actually changes self.shared.shared settings!
+        # unfortunately if we don't have it run, then other settings than the target ones
+        # are not initialized correctly ...
         _status, _error = self.shared.receive_data()

+        # if we intervene with MyAuthentifierSharedData checking here, we'll mess up initialization;
+        # so instead, re-enforce the target settings afterwards
+
+        # this is likely the response to initial connection from RDP client,
+        # which instructs RDP proxy to serve the starting blue screen
         device = "<host>$<application path>$<working dir>$<args> for Application"
         login = self.shared.get(u'login', MAGICASK) or MAGICASK
         host = self.shared.get(u'real_target_device', MAGICASK) or MAGICASK
@@ -271,6 +299,12 @@ class ACLPassthrough():
             self.shared.shared[u'real_target_device'] = host
             kv = interactive_data

+        if isinstance(self.shared, MyAuthentifierSharedData):
+            # re-enforce target settings
+            self.shared.target_settings_enforce_hardcoded()
+
+        print("passthrough start self.shared: {}".format(self.shared.shared))
+
         # selector_data = {
         #     u'target_login': 'Proxy\\Administrator\x01login 2\x01login 3',
         #     u'target_device': '10.10.44.27\x01device 2\x01device 3',
@@ -283,7 +317,10 @@ class ACLPassthrough():
         kv[u'record_filebase'] = datetime.now().strftime("%Y-%m-%d/%H:%M-") + str(uuid.uuid4())
         kv[u'login'] = self.shared.get(u'target_login')
         kv[u'proto_dest'] = "RDP"
-        kv[u'target_port'] = "3389"
+        if not( isinstance(self.shared, MyAuthentifierSharedData) ):
+            kv[u'target_port'] = "3389"
+        else:
+            kv[u'target_port'] = self.shared.get(u'target_port')
         kv[u'session_id'] = str(datetime.now())
         kv[u'module'] = 'RDP' if self.shared.get(u'login') != 'internal' else 'INTERNAL'
         kv[u'target_password'] = self.shared.get(u'target_password')

With this in place, the procedure goes like this:

  1. On PC 2 (192.168.RDP.PROXY), start passthrough.py in one terminal shell (with absolute path): $HOME/src/redemption.git/tools/passthrough/passthrough.py
  2. On PC 2 (192.168.RDP.PROXY), start rdpproxy in another terminal shell: rdpproxy -nf
  3. On PC 1 (192.168.RDP.CLIENT), start remmina
  4. Enter the actual IP address corresponding to 192.168.RDP.PROXY in remmina main bar under "RDP", and hit ENTER remmina1
    • note that this will use a default screen viewer resolution, which results with videos 608x448 pixels in size; otherwise make a "New connection profile" and set Basic/Resolution/Custom there
  5. You should be presented with the "RDP Blue Screen" now in remmina; since passthrough.py is hacked to enforce target settings here, you can enter whatever here - my preference these days is type "a" in all fields, and hit ENTER: remmina2
  6. At this point, the remmina window will likely shut down; the explanation is in the rdpproxy log:
    rdpproxy: INFO (3101423/3101423) -- New Module: MODULE_RDP
    rdpproxy: ERR (3101423/3101423) -- OutCryptoTransport::open : open failed (/path/to/extern_disk/rdpproxy/recorded//2023-02-06/07:58-e6565258-cb31-4adf-8034-2a578c4c735e.logred-Y8Mc2h.tmp -> /path/to/extern_disk/rdpproxy/recorded//2023-02-06/07:58-e6565258-cb31-4adf-8034-2a578c4c735e.log): No such file or directory
    rdpproxy: [RDP Session] session_id="2023-02-06 07:58:05.611350" client_ip="192.168.RDP.CLIENT" target_ip="209.85.RDP.SERVER.com" user="TARGET_USERNAME" device="209.85.RDP.SERVER.com" service="" account="TARGET_USERNAME" type="SESSION_CREATION_FAILED"
    rdpproxy: ERR (3101423/3101423) -- OutCryptoTransport::do_send failed: file not opened (/path/to/extern_disk/rdpproxy/recorded//2023-02-06/07:58-e6565258-cb31-4adf-8034-2a578c4c735e.logred-Y8Mc2h.tmp->)
    rdpproxy: INFO (3101423/3101423) -- Socket Authentifier (5) : closing connection
    rdpproxy: INFO (3101423/3101423) -- Client Session Disconnected
    rdpproxy: [rdpproxy] psid="15482383101423" user="TARGET_USERNAME" type="DISCONNECT"
    rdpproxy: INFO (3101423/3101423) -- Socket RDP Client (6) : closing connection

    The connection failed, because code wanted to write in subdirectory 2023-02-06 (current day) in /path/to/extern_disk/rdpproxy/recorded, but the directory does not exist (and code did not create it); thankfully rdpproxy does not crash because of this, and keeps running; remmina will however close the window and terminate the session

  7. So, on PC 2 (192.168.RDP.PROXY), create "day" folders (here 2023-02-06) in /path/to/extern_disk/rdpproxy/recorded and /path/to/extern_disk/rdpproxy/hash (yes, that one will fail, too), and give them "liberal" permissions (rdpproxy and passthrough.py can stay running during this):
    mkdir -p /path/to/extern_disk/rdpproxy/{recorded,hash}/2023-02-06
    chmod -R a+r,ug+w /path/to/extern_disk/rdpproxy
  8. Repeat the procedure with remmina: enter "a" in all fields at the "RDP blue screen" and hit ENTER - and ... I didn't experience this yesterday, not sure what changed, but today remmina terminates the session again, the reason being:

    rdpproxy: ERR (3104008/3104008) -- OutCryptoTransport::open: open failed hash file /path/to/extern_disk/rdpproxy/hash//2023-02-06/08:15-0e3a435a-614e-4f86-b41a-48b486717898.log: Permission denied
    rdpproxy: INFO (3104008/3104008) -- Socket Authentifier (5) : closing connection
    rdpproxy: INFO (3104008/3104008) -- Client Session Disconnected

    The weird thing is that the file actually did get created, but with very tight permissions:

    $ ls -la /path/to/extern_disk/rdpproxy/hash/2023-02-06/
    total 12
    drwxrwxr-x 2 user user 4096 Feb  6 08:15 .
    drwxrwxrwx 4 user user 4096 Feb  6 08:11 ..
    -r--r----- 1 user user  237 Feb  6 08:15 08:15-0e3a435a-614e-4f86-b41a-48b486717898.log

    Since it has an UUID in the name which changes, there is no chance we could do chmod on an active log file after failure and fix it that way; however it seems that this patch to src/transport/crypto_transport.cpp works:

    diff --git a/src/transport/crypto_transport.cpp b/src/transport/crypto_transport.cpp
    index 997b6c54e..0255bc940 100644
    --- a/src/transport/crypto_transport.cpp
    +++ b/src/transport/crypto_transport.cpp
    @@ -756,6 +756,7 @@ void OutCryptoTransport::open(const char * finalname, const char * const hash_fi
      {
          size_t base_len = 0;
          const char * base = basename_len(finalname, base_len);
    +    file_permissions = FilePermissions(0664);
          this->open(finalname, hash_filename, file_permissions, {base, base_len});
      }
    
    @@ -808,8 +809,10 @@ void OutCryptoTransport::create_hash_file(HashArray const & qhash, HashArray con
          ocrypto hash_encrypter(this->cctx, this->rnd);
          OutFileTransport hash_out_file(unique_fd(::open(
              this->hash_filename.c_str(),
    -        O_WRONLY | O_CREAT,
    -        S_IRUSR | S_IRGRP)));
    +        //O_WRONLY | O_CREAT,
    +        //S_IRUSR | S_IRGRP)));
    +        O_RDWR | O_CREAT,
    +        S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH)));
          if (!hash_out_file.is_open()){
              int const err = errno;
              LOG(LOG_ERR, "OutCryptoTransport::open: open failed hash file %s: %s", this->hash_filename, strerror(err));
  9. After the above change is made, code compiled, new deb generated and installed, again - repeat the procedure with remmina: enter "a" in all fields at the "RDP blue screen" and hit ENTER - and FINALLY we get connection to the target server, and the remote desktop is shown in the remmina window! And that means, in this case, also recording has started ...
  10. Do what you want to do in the remote session, and then disconnect/close the remmina window; the recording should now stop, and we look for new files in our daily "recorded" folder, in this case /path/to/extern_disk/rdpproxy/recorded/2023-02-06"

    $ ls -la /path/to/extern_disk/rdpproxy/recorded/2023-02-06/*wrm*
    -rw-rw-r-- 1 user user 585660 Feb  6 09:20 /path/to/extern_disk/rdpproxy/recorded/2023-02-06/09:19-b4927384-5a46-4eff-b0bb-58c2d818b533-000000.wrm
    -rw-rw-r-- 1 user user    350 Feb  6 09:20 /path/to/extern_disk/rdpproxy/recorded/2023-02-06/09:19-b4927384-5a46-4eff-b0bb-58c2d818b533.mwrm
    
    $ cat /path/to/extern_disk/rdpproxy/recorded/2023-02-06/09:19-b4927384-5a46-4eff-b0bb-58c2d818b533.mwrm
    v2
    1024 768
    checksum
    
    /path/to/extern_disk/rdpproxy/recorded//2023-02-06/09:19-b4927384-5a46-4eff-b0bb-58c2d818b533-000000.wrm 585660 33204 1000 1000 2049 14942263 1675671600 1675671600 1675671579 1675671601 1f72d42f9105512c0c6152b8a69892421b8c6717738462af10d3443d3dacb30e ce08419b4f71355972487147c3f77bb09733436430833ca38c7cc6b93bcb4527

    We can also note, that the .mwrm file is a text file, that contains a reference to the .wrm file.

  11. We are now ready to perform conversion of the .(m)wrm file to .mp4 video file using redrec. The default settings there feature a compression a bit too heavy for my taste, and also I'd want the settings from Encoding video for the web gist; so my conversion command line is this (note I wanted to specify -an to disable audio stream, but I couldn't tell how to do that with --video-codec-options, so I gave if an=1 which should be converted to the incorrect -an=1, however, it seems to work, as there are no errors or warnings due to this, and there is no autio in the output either):
    redrec --encryption=disable --video=on --full=on --frame-rate=25 \
    --video-codec-options="pix_fmt=yuv420p profile:v=baseline level=3 an=1" \
    -i "/path/to/extern_disk/rdpproxy/recorded/2023-02-06/09:19-b4927384-5a46-4eff-b0bb-58c2d818b533.mwrm" \
    -o $PWD/test-000000.mp4

    ... with the expected command output in terminal for succesful conversion being:

    Output file is "$HOME/Desktop/test-000000.mp4".
    redrec: INFO (3120001/3120001) -- player begin_capture = 0
    redrec: INFO (3120001/3120001) -- Enable capture:  wrm=no  png=no  kbd=no  video=yes  video_full=yes  pattern=no  ocr=no  meta=no

    Let's check the resulting files created:

    $ ls -la test*
    -r--r----- 1 user user 2171411 Feb  6 09:44 test-000000-000000.mp4
    -rw-rw-r-- 1 user user    2624 Feb  6 09:44 test-000000-000000.png
    -r--r----- 1 user user 2825884 Feb  6 09:44 test-000000.mp4
    -r--r----- 1 user user      37 Feb  6 09:44 test-000000.pgs

    We can observe that:

    • there are two mp4 file; one called test-000000.mp4 (as specified), the other called test-000000-000000.mp4
    • We can see that redrec seems to add -00000x as suffix to the filename basename, which is likely related to the 10 minute chunks; --full=on in the options seems to force creation of a singe .mp4 file with a name as specified in command line with -o argument
    • (earlier, I used to get permission errors on the .pgs file, but it seems the above patch fixed that) The two mp4 files are slightly different from one another - but we can see that it is the file that has the name as specified on the command line (without suffix) that has the ffmpeg setting that we specified on the command line (also, I had set the client screen resolution to 1024x768 beforehand):
      $ for ix in *.mp4; do echo $ix "`ffprobe $ix 2>&1 | grep -A1 '^  Duration:'`"; done
      test-000000-000000.mp4   Duration: 00:00:20.44, start: 0.760000, bitrate: 849 kb/s
      Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1024x768, 847 kb/s, 15.85 fps, 25 tbr, 12800 tbn, 50 tbc (default)
      test-000000.mp4   Duration: 00:00:21.60, start: 0.040000, bitrate: 1046 kb/s
      Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1024x768, 1043 kb/s, 25.05 fps, 25 tbr, 12800 tbn, 50 tbc (default)

Well, from what I could see, the resulting videos are very decent and responsive - so thanks again for a great software!

jonathanpoelen commented 1 year ago

Your scheme is good. I didn't know Mermaid, it's nice

For rdpproxy.ini, anything not listed by --print-default-ini will be ignored. The ini variable of the sources is the grouping of the variables read from rdpproxy.ini and those sent by passthrough.py. The variables that can be exchanged with the latter are listed in projects/redemption_configs/autogen/doc/sesman_dialog.txt and the list of all variables is in projects/redemption_configs/configs_specs/config_spec.hpp (there is a lot of info, but not all of it will be useful to you; such as the one indicating the origin of the option (connpolicy*)) In this file sesman = passthrough.py, ini = the file rdpproxy.ini, gui = ini + origin of the option.

Normally you don't need allow_rt_without_recording=1 to have a record. You just need is_rec=1 to be sent by passthrough.py along with the target address and [video] capture_flags to be initialized with wrm. I feel like this is more of an unwelcome side effect.

So, it seems that by default, these settings have break_interval = 600 which is "Time between 2 wrm movies", which as I understanding, means that the video capture will happen in 10 minute chunks (probably to allow for long captures, so you only lose the last 10 minutes if you run out of space on disk).

In reality, even with a space problem, the file can be read down to the last recorded packet and the video generated. But the last second will be missing and the mwrm will have to be completed manually. The split is used so that redrec ignores some files with the --begin / --end options and avoids too large files.

The connection failed, because code wanted to write in subdirectory 2023-02-06 (current day) in /path/to/extern_disk/rdpproxy/recorded

We should be a bit more flexible on this and have the proxy create the folder hierarchy by itself if needed or have passthrough.py do it. Actually, the sub-folder is not a necessity, it is passthrough.py that sends it through the record_filebase variable.

the problem with the crypto_transport.cpp step is strange, as you say, the uuid is used to avoid this situation. Historically, the only way to get this kind of error is to get a screen resize (client opens with a certain screen size, server asks for another size), but we've had an option enabled by default for that for a while.

Regarding the redrec options, --video=on and --full=on are two independent options. The first one makes a cut based on the detection of window titles (it doesn't always detect them). By setting only --full, there will be only one file.