jborean93 / omi

Open Management Infrastructure
Other
111 stars 13 forks source link

Connection to Windows Server incredibly slow #54

Closed Yannik closed 2 years ago

Yannik commented 2 years ago

Hi @jborean93,

first of all, thank you so much for taking on this project! I'm really glad I found it, because I've always had to use a VM to work on windows servers, but with this, that could be a thing of the past.

Unfortunately, I have been having some troubles: The execution of command is really really slow. A simple Write-Host Hello on a remote windows server takes about 5 seconds. Working interactively like this is pretty much impossible.

I have tried extensively to debug and resolve this problem.

The test environment

First of all, to make this as reproducable as possible, I have built a test environment in docker like this:

  1. Create docker container: docker run -it debian:bullseye /bin/bash
  2. In the docker container, setup everything:
    
    apt update
    apt install -y libicu67 wget openssl sudo krb5-user
    wget https://github.com/PowerShell/PowerShell/releases/download/v7.2.6/powershell-lts_7.2.6-1.deb_amd64.deb
    dpkg -i powershell-lts_7.2.6-1.deb_amd64.deb

sudo pwsh -Command 'Install-Module PSWSMan -confirm:$false -Force' sudo pwsh -Command 'Install-WSMan'

echo ' [realms] DOMAIN.COM = { kdc = dc1.domain.com admin_server = dc1.domain.com } [domain_realm] .domain.com = DOMAIN.COM ' > /etc/krb5.conf

Enter-PSSession -Credential (Get-Credential) dc1.domain.com -Authentication Kerberos



3. For additional debugging power, I have: 
     - configured logging in `/opt/omi/etc/omicli.conf`
     - created the `/opt/omi/var/log` directory
     - run `export KRB5_TRACE=/dev/stdout` before running `pwsh`
     - run `tcpdump`

# Potential issue no. 1
By looking at the packet capture and the `KRB5_TRACE` output, I was able to identity the following:

The kerberos client would issue numerous requests to `_kerberos.DOMAIN.COM`,  `_kerberos-master._udp.DOMAIN.COM` and  `_kerberos-master._tcp.DOMAIN.COM`, which would be answered with `NXDOMAIN` by the DNS server (which is also the dc I am trying to connect to). These records do not exist on a windows server.

I not sure why it's doing that, as I have already specified the kdc in `krb5.conf`. As far as I know, the correct SRV lookups would be 

I was able to get rid of these lookups by also setting `master_kdc = dc.1domain.com` in `krb5.conf` 

If you have any idea why it's doing *these* lookups instead of `_kerberos._udp.DOMAIN.COM` and `_kerberos._tcp.DOMAIN.COM` (which it should look up as far as I know), please let me know.

# Potential issue no. 2

There were quite many `Request or response is too big for UDP; retrying with TCP` messages in the `KRB5_TRACE` output.

Not sure if this is to be expected, I was however be able to get rid of that by setting `krb5.forcetcp = true` in the `libdefaults` section in `krb5.conf`

# Potential issue no .3

What seems really odd to me, is that during the initial `Enter-PSSession`, there are 144(!) requests to the server in the `KRB5_TRACE`. 

Executing `Write-Host Test` results in 72 requests. 

I wasn't really able to identify why it's doing that. But it does seem excessive to me.

(I believe that this is also responsible for the vast amount of DNS loglines I was seeing in the potential issue #1, as I believe one DNS lookup was done for each request.)

# Current state

Unfortunately, while the aforementioned mitigations #1 and #2 fixed the error messages I was gettting, the connection is still as slow as before. I have tested this with multiple windows domains, different server versions etc.

I have captured lots of logs about this issue; would it possible for me to share them with you privately?
jborean93 commented 2 years ago

The crux of the issue with omi is that it is super inefficient which is compounded by the fact that for every single HTTP request it will re-authenticate rather than trying to re-use the socket/session. This is bad because WinRM is quite a chatty protocol, to just run one command you have the following that needs to be sent:

Depending on how long the shell/command is open it could be sending multiple receive requests after a set amount of time. If the command is large then it can also send them in multiple messages. Each one of the requests from the client is re-authenticating by attaching a new Kerberos token which means it needs to;

The lookup phase can be quite inefficient if DNS is used and is not set up in a way to make that easily done. This essentially means if any of the stages to build the HTTP request; kerb, DNS, network latency, is slow it is compounded quite dramatically and creates a slow session that you are seeing.

So to try and speed things up I recommend you:

[libdefaults]
  # I'm not sure if this is actually needed or whether the manual mapping
  # below is enough
  dns_lookup_kdc = false

[realms]
  DOMAIN.COM = {
    kdc = dc.domain.com
  }

[domain_realm]
  .domain.com = DOMAIN.COM
  domain.com = DOMAIN.COM

The 2nd step isn't usually too bad but can be a source of potential slowdowns. I'm not sure about the TCP side as I've never encountered it before. Potentially the krb5 token is just too large and needs to use TCP.

This should remove any ambiguity and remove the DNS lookups that happen at runtime. You can also set dns_lookup_kdc = false in [libdefaults] to disable the DNS lookup altogether.

While I would love to fix up omi to be more efficient and re-use a socket/session to avoid all these re-authentication requests this is not a trivial task as it would require the whole library to be re-worked. I prefer to just rewrite the WSMan client in C# for PowerShell to use. I've actually started on this task but I can only work on it in my spare time so it is slow going. The actual WSMan bits are done, the tricky part is getting it to work in PowerShell without changes on its end.

Yannik commented 2 years ago

Hey @jborean93,

Each one of the requests from the client is re-authenticating by attaching a new Kerberos token which means it needs to;

  • lookup the KDC
  • optionally authenticate itself with the password if a password is set
  • ask the KDC for a ticket to the server

wow, that's extremely inefficient indeed.

As far as I understand you, 140 requests to the KDC simply for Enter-PSession, and 70 requests per command is to be expected in this case?

This should remove any ambiguity and remove the DNS lookups that happen at runtime. You can also set dns_lookup_kdc = false in [libdefaults] to disable the DNS lookup altogether.

Thanks for the tip! I already had the domain_realm configured, but I have now added dns_lookup_kdc = false and dns_lookup_realm = false to the krb5.conf and used kinit instead of credentials for Enter-PSSession. This improved things a little bit.

In addition to that, I have installed a debian machine on-site of the windows server, ssh'd to that and went from there to the dc with Enter-PSSession instead of connecting from my local machine to the dc via vpn. I thought that with this amount of (synchronous) requests, removing a little bit of latency (in this case about 40ms) would help.. That actually improved things quite a bit more!

Entering/Exiting a session still takes a few seconds, but executing commands is mostly fine. Tab-Completing cmdlets still takes really long.

Tab-Completion of parameters seems completely broken. Is this the same for you? Do you know the cause of this?

Unfortunately, using an intermediary server kills the benefit of being able to directly copy files from my machine to a server and such, which would've been great.

While I would love to fix up omi to be more efficient and re-use a socket/session to avoid all these re-authentication requests this is not a trivial task as it would require the whole library to be re-worked. I prefer to just rewrite the WSMan client in C# for PowerShell to use. I've actually started on this task but I can only work on it in my spare time so it is slow going. The actual WSMan bits are done, the tricky part is getting it to work in PowerShell without changes on its end.

That is awesome. I will gladly help with testing.

jborean93 commented 2 years ago

As far as I understand you, 140 requests to the KDC simply for Enter-PSession, and 70 requests per command is to be expected in this case?

I'm not sure on the exact number but I am honestly not surprised at that amount as Enter-PSSession by itself has a lot of actual HTTP requests that occur. There's not just the WSMan shell/command creation but there are some initial commands that Enter-PSSession runs before it gives you an actual shell..

Tab-Completion of parameters seems completely broken. Is this the same for you? Do you know the cause of this?

Yea I've encountered this, it sometimes works but then just outright does nothing. I've not looked into it enough to try and figure out what is going wrong unfortunately. It's one of those I don't want to spend too much more time on a library that doesn't really have much of a future. I prefer to get the actual client in a better state before looking at all that.

Sorry that I couldn't help you further with this. If you do have any other things that helped improved the performance for you then please share it if you can for others to see. I will certainly keep this thread in mind when I have a working implementation in C#. For now I'll close the issue as there's nothing I know off that can be done or that I am planning on doing for this particular repo.

Yannik commented 2 years ago

Thank you, @jborean93!

Yannik commented 1 year ago

Hey Jordan,

I hope you are well.

Is there perhaps any progress on your wsman client project? I'm still very much interested :)

Yannik

jborean93 commented 1 year ago

There is https://github.com/jborean93/PSWSMan. Unfortunately it's held up by a dependency not supporting dotnet 7 yet so it won't work normally on PowerShell 7.3. It works just fine for pwsh 7.2 though.

Yannik commented 1 year ago

Ah, great! I will immediately test it out :-)

I'm a bit confused about the installation instructions though: It says to run Install-Module -Name PSWSMan, but that installs the PSWSMan from the your omi repo...?

jborean93 commented 1 year ago

Yea, the aim is to create the next release for PSWSMan to be based on the contents of that repo but due to the problem with dotnet 7 I've had to hold off but haven't updated the docs to properly reflect that. What you should do is

pwsh -File ./build.ps1 -Configuration Release -Task Build

This will create a copy of the module under output/PSWSMan which you can import to test with:

Import-Module ./output/PSWSMan
Enable-PSWSMan -Force

Note you will need the dotnet SDK to compile the code.

Yannik commented 1 year ago

Got it! I can successfully Enter-PSSession.

Unfortunately, the "slowness" doesn't really seem to be improved too much compared to omi. A simple cd to a local directory takes about 2.5 seconds. (The time the command itself takes is negligible, it's about 30ms according to Measure-Command).

(I have removed the omi PSWSMan prior to installing this one. If I don't Enable-PSWSMan I get an This parameter set requires WSMan, and no supported WSMan client library was found error message, so I am pretty sure it is using your module.)

Yannik commented 1 year ago

Let me explain exactly what I am doing. I am using these two commands:

  1. Enter-PSSession -Credential (Get-Credential) dc1.domain.com -Authentication Kerberos (takes about 9 seconds)
  2. cd .. (takes about 2.5 seconds)

I have done two things to debug this:

1. debug kerberos with export KRB5_TRACE=/dev/stdout

Enter-PSSession:

``` [1965171] 1674732677.654930: Resolving unique ccache of type MEMORY [1965171] 1674732677.654931: Getting initial credentials for administrator@DOMAIN.COM [1965171] 1674732677.654933: Sending unauthenticated request [1965171] 1674732677.654934: Sending request (224 bytes) to DOMAIN.COM [1965171] 1674732677.654935: Resolving hostname dc1.domain.com [1965171] 1674732677.654936: Sending initial UDP request to dgram 10.1.1.1:88 [1965171] 1674732677.654937: Received answer (213 bytes) from dgram 10.1.1.1:88 [1965171] 1674732677.654938: Response was not from primary KDC [1965171] 1674732677.654939: Received error from KDC: -1765328359/Additional pre-authentication required [1965171] 1674732677.654942: Preauthenticating using KDC method data [1965171] 1674732677.654943: Processing preauth types: PA-PK-AS-REQ (16), PA-PK-AS-REP_OLD (15), PA-ETYPE-INFO2 (19), PA-ENC-TIMESTAMP (2) [1965171] 1674732677.654944: Selected etype info: etype aes256-cts, salt "DOMAIN.COMAdministrator", params "" [1965171] 1674732677.654945: PKINIT client has no configured identity; giving up [1965171] 1674732677.654946: PKINIT client has no configured identity; giving up [1965171] 1674732677.654947: Preauth module pkinit (16) (real) returned: 22/Invalid argument [1965171] 1674732677.654948: AS key obtained for encrypted timestamp: aes256-cts/EA3F [1965171] 1674732677.654950: Encrypted timestamp (for 1674732677.256704): plain 301AA011180F32303233303132363131333131375AA105020303EAC0, encrypted 6A4A0386C38C46370CB6E7BC9DF28D377DEF2D183AA4E5C7921E94F938B35973917DCE74E9569E72DF6B5F1A00F0F550E2B0C34BC376B211 [1965171] 1674732677.654951: Preauth module encrypted_timestamp (2) (real) returned: 0/Success [1965171] 1674732677.654952: Produced preauth for next request: PA-ENC-TIMESTAMP (2) [1965171] 1674732677.654953: Sending request (304 bytes) to DOMAIN.COM [1965171] 1674732677.654954: Resolving hostname dc1.domain.com [1965171] 1674732677.654955: Sending initial UDP request to dgram 10.1.1.1:88 [1965171] 1674732678.323111: Received answer (114 bytes) from dgram 10.1.1.1:88 [1965171] 1674732678.323112: Response was not from primary KDC [1965171] 1674732678.323113: Received error from KDC: -1765328332/Response too big for UDP, retry with TCP [1965171] 1674732678.323114: Request or response is too big for UDP; retrying with TCP [1965171] 1674732678.323115: Sending request (304 bytes) to DOMAIN.COM (tcp only) [1965171] 1674732678.323116: Resolving hostname dc1.domain.com [1965171] 1674732678.323117: Initiating TCP connection to stream 10.1.1.1:88 [1965171] 1674732678.323118: Sending TCP request to stream 10.1.1.1:88 [1965171] 1674732679.018757: Received answer (1817 bytes) from stream 10.1.1.1:88 [1965171] 1674732679.018758: Terminating TCP connection to stream 10.1.1.1:88 [1965171] 1674732679.018759: Response was not from primary KDC [1965171] 1674732679.018760: Processing preauth types: PA-ETYPE-INFO2 (19) [1965171] 1674732679.018761: Selected etype info: etype aes256-cts, salt "DOMAIN.COMAdministrator", params "" [1965171] 1674732679.018762: Produced preauth for next request: (empty) [1965171] 1674732679.018763: AS key determined by preauth: aes256-cts/EA3F [1965171] 1674732679.018764: Decrypted AS reply; session key is: aes256-cts/3768 [1965171] 1674732679.018765: FAST negotiation: unavailable [1965171] 1674732679.018766: Initializing MEMORY:qAq81b2 with default princ administrator@DOMAIN.COM [1965171] 1674732679.018767: Storing administrator@DOMAIN.COM -> krbtgt/DOMAIN.COM@DOMAIN.COM in MEMORY:qAq81b2 [1965171] 1674732679.018768: Storing config in MEMORY:qAq81b2 for krbtgt/DOMAIN.COM@DOMAIN.COM: pa_type: 2 [1965171] 1674732679.018769: Storing administrator@DOMAIN.COM -> krb5_ccache_conf_data/pa_type/krbtgt\/DOMAIN.COM\@DOMAIN.COM@X-CACHECONF: in MEMORY:qAq81b2 [1965171] 1674732679.018773: Getting credentials administrator@DOMAIN.COM -> host/dc1.domain.com@ using ccache MEMORY:qAq81b2 [1965171] 1674732679.018774: Retrieving administrator@DOMAIN.COM -> krb5_ccache_conf_data/start_realm@X-CACHECONF: from MEMORY:qAq81b2 with result: -1765328243/Matching credential not found [1965171] 1674732679.018775: Retrieving administrator@DOMAIN.COM -> host/dc1.domain.com@ from MEMORY:qAq81b2 with result: -1765328243/Matching credential not found [1965171] 1674732679.018776: Retrying administrator@DOMAIN.COM -> host/dc1.domain.com@DOMAIN.COM with result: -1765328243/Matching credential not found [1965171] 1674732679.018777: Retrieving administrator@DOMAIN.COM -> krbtgt/DOMAIN.COM@DOMAIN.COM from MEMORY:qAq81b2 with result: 0/Success [1965171] 1674732679.018778: Starting with TGT for client realm: administrator@DOMAIN.COM -> krbtgt/DOMAIN.COM@DOMAIN.COM [1965171] 1674732679.018779: Requesting tickets for host/dc1.domain.com@DOMAIN.COM, referrals on [1965171] 1674732679.018780: Generated subkey for TGS request: aes256-cts/5961 [1965171] 1674732679.018781: etypes requested in TGS request: aes256-cts, aes256-sha2, camellia256-cts, aes128-sha2, aes128-cts, camellia128-cts [1965171] 1674732679.018783: Encoding request body and padata into FAST request [1965171] 1674732679.018784: Sending request (1978 bytes) to DOMAIN.COM [1965171] 1674732679.018785: Resolving hostname dc1.domain.com [1965171] 1674732679.018786: Initiating TCP connection to stream 10.1.1.1:88 [1965171] 1674732679.018787: Sending TCP request to stream 10.1.1.1:88 [1965171] 1674732679.018788: Received answer (1988 bytes) from stream 10.1.1.1:88 [1965171] 1674732679.018789: Terminating TCP connection to stream 10.1.1.1:88 [1965171] 1674732679.018790: Response was not from primary KDC [1965171] 1674732679.018791: Decoding FAST response [1965171] 1674732679.018792: FAST reply key: aes256-cts/503E [1965171] 1674732679.018793: TGS reply is for administrator@DOMAIN.COM -> host/dc1.domain.com@DOMAIN.COM with session key aes256-cts/3442 [1965171] 1674732679.018794: TGS request result: 0/Success [1965171] 1674732679.018795: Received creds for desired service host/dc1.domain.com@DOMAIN.COM [1965171] 1674732679.018796: Storing administrator@DOMAIN.COM -> host/dc1.domain.com@ in MEMORY:qAq81b2 [1965171] 1674732679.018798: Creating authenticator for administrator@DOMAIN.COM -> host/dc1.domain.com@, seqnum 939092154, subkey aes256-cts/66BC, session key aes256-cts/3442 [1965171] 1674732680.226539: Read AP-REP, time 1674732680.18799, subkey aes256-cts/3891, seqnum 867625445 [1965171] 1674732681.205869: Getting credentials administrator@DOMAIN.COM -> host/dc1.domain.com@ using ccache MEMORY:qAq81b2 [1965171] 1674732681.205870: Retrieving administrator@DOMAIN.COM -> krb5_ccache_conf_data/start_realm@X-CACHECONF: from MEMORY:qAq81b2 with result: -1765328243/Matching credential not found [1965171] 1674732681.205871: Retrieving administrator@DOMAIN.COM -> host/dc1.domain.com@ from MEMORY:qAq81b2 with result: 0/Success [1965171] 1674732681.205873: Creating authenticator for administrator@DOMAIN.COM -> host/dc1.domain.com@, seqnum 764782148, subkey aes256-cts/5A95, session key aes256-cts/3442 [1965171] 1674732681.205878: Read AP-REP, time 1674732682.205874, subkey aes256-cts/F0ED, seqnum 859961980 [1965171] 1674732682.986757: Getting credentials administrator@DOMAIN.COM -> host/dc1.domain.com@ using ccache MEMORY:qAq81b2 [1965171] 1674732682.986758: Retrieving administrator@DOMAIN.COM -> krb5_ccache_conf_data/start_realm@X-CACHECONF: from MEMORY:qAq81b2 with result: -1765328243/Matching credential not found [1965171] 1674732682.986759: Retrieving administrator@DOMAIN.COM -> host/dc1.domain.com@ from MEMORY:qAq81b2 with result: 0/Success [1965171] 1674732682.986761: Creating authenticator for administrator@DOMAIN.COM -> host/dc1.domain.com@, seqnum 298660106, subkey aes256-cts/0FDE, session key aes256-cts/3442 [1965171] 1674732683.006651: Read AP-REP, time 1674732683.986762, subkey aes256-cts/698B, seqnum 844037237 [1965171] 1674732684.754112: Getting credentials administrator@DOMAIN.COM -> host/dc1.domain.com@ using ccache MEMORY:qAq81b2 [1965171] 1674732684.754113: Retrieving administrator@DOMAIN.COM -> krb5_ccache_conf_data/start_realm@X-CACHECONF: from MEMORY:qAq81b2 with result: -1765328243/Matching credential not found [1965171] 1674732684.754114: Retrieving administrator@DOMAIN.COM -> host/dc1.domain.com@ from MEMORY:qAq81b2 with result: 0/Success [1965171] 1674732684.754116: Creating authenticator for administrator@DOMAIN.COM -> host/dc1.domain.com@, seqnum 520633777, subkey aes256-cts/54E2, session key aes256-cts/3442 [1965171] 1674732685.145317: Read AP-REP, time 1674732685.754117, subkey aes256-cts/FF01, seqnum 1029217967 [1965171] 1674732686.905766: Getting credentials administrator@DOMAIN.COM -> host/dc1.domain.com@ using ccache MEMORY:qAq81b2 [1965171] 1674732686.905767: Retrieving administrator@DOMAIN.COM -> krb5_ccache_conf_data/start_realm@X-CACHECONF: from MEMORY:qAq81b2 with result: -1765328243/Matching credential not found [1965171] 1674732686.905768: Retrieving administrator@DOMAIN.COM -> host/dc1.domain.com@ from MEMORY:qAq81b2 with result: 0/Success [1965171] 1674732686.905770: Creating authenticator for administrator@DOMAIN.COM -> host/dc1.domain.com@, seqnum 859773839, subkey aes256-cts/22DF, session key aes256-cts/3442 [1965171] 1674732687.290201: Read AP-REP, time 1674732687.905771, subkey aes256-cts/C950, seqnum 1072142560 ```

cd ..:

``` [1965171] 1674732719.147079: Getting credentials administrator@DOMAIN.COM -> host/dc1.domain.com@ using ccache MEMORY:qAq81b2 [1965171] 1674732719.147080: Retrieving administrator@DOMAIN.COM -> krb5_ccache_conf_data/start_realm@X-CACHECONF: from MEMORY:qAq81b2 with result: -1765328243/Matching credential not found [1965171] 1674732719.147081: Retrieving administrator@DOMAIN.COM -> host/dc1.domain.com@ from MEMORY:qAq81b2 with result: 0/Success [1965171] 1674732719.147083: Creating authenticator for administrator@DOMAIN.COM -> host/dc1.domain.com@, seqnum 836218381, subkey aes256-cts/94EE, session key aes256-cts/3442 [1965171] 1674732719.147088: Read AP-REP, time 1674732720.147084, subkey aes256-cts/14C3, seqnum 1222013283 [1965171] 1674732720.579188: Getting credentials administrator@DOMAIN.COM -> host/dc1.domain.com@ using ccache MEMORY:qAq81b2 [1965171] 1674732720.579189: Retrieving administrator@DOMAIN.COM -> krb5_ccache_conf_data/start_realm@X-CACHECONF: from MEMORY:qAq81b2 with result: -1765328243/Matching credential not found [1965171] 1674732720.579190: Retrieving administrator@DOMAIN.COM -> host/dc1.domain.com@ from MEMORY:qAq81b2 with result: 0/Success [1965171] 1674732720.579192: Creating authenticator for administrator@DOMAIN.COM -> host/dc1.domain.com@, seqnum 118455995, subkey aes256-cts/521D, session key aes256-cts/3442 [1965171] 1674732720.579197: Read AP-REP, time 1674732721.579193, subkey aes256-cts/A851, seqnum 1274203456 ```

2. packet capture

enter-pssession.zip

cd.zip

Can you make any sense of this?

jborean93 commented 1 year ago

Can you try without Kerberos altogether to rule that out as being slow? The simplest way to test is by using the Devolutions authentication provider like so

$so = New-PSWSManSessionOption -AuthProvider Devolutions -AuthMethod NTLM
Enter-PSSession ... -SessionOption $so

The output from your krb5_trace shows about 10 seconds for setting up the session and a bit less than 2 seconds for a command. In particular there is a > 1 second time frame between

[1965171] 1674732719.147088: Read AP-REP, time 1674732720.147084, subkey aes256-cts/14C3, seqnum 1222013283
[1965171] 1674732720.579188: Getting credentials administrator@DOMAIN.COM -> host/dc1.domain.com@ using ccache MEMORY:qAq81b2

I don't fully know what Kerberos is doing between these steps but it shouldn't be taking that long normally as far as I know. While this library is not as fast as the native C code you get on Windows it shouldn't be as slow as you are saying, I certainly don't see that locally.

Yannik commented 1 year ago

I have just tested it like this:

$so = New-PSWSManSessionOption -AuthProvider Devolutions -AuthMethod NTLM
Enter-PSSession -SessionOption $so -Credential (Get-Credential) dc1.domain.com
cd ..

The initial Enter-PSSession is about 1 second faster (8 seconds). Running commands is about the same (3 seconds).

I have done packet captures for both Enter-PSSession and cd ..: ntlm-pcaps.zip

jborean93 commented 1 year ago

Thanks for sharing the info, unfortunately the data is encrypted so I cannot really see what the messages are in the wireshark capture. The only thing I can gather is that each roundtrip to the server takes a bit less than 500 milliseconds which is somewhat slow. There are a lot of roundtrips needed for setting up the runspace as well as running a new command.

Is this slow at all when using a Windows client with Enter-PSSession? What about if you have Python and run the following on the same Linux host as the one that's slow with PSWSMan.

# Needs 'python -m pip install pypsrp'

from pypsrp.client import Client

with Client("server", username="user", password="password", port=5985) as client:
    output, streams, had_errors = client.execute_ps('"testing"')
    print(output)

Another thing you can try is to run this PowerShell code that replicates the first WSMan payload that is sent

$username = 'vagrant'
$password = 'vagrant'
$basicString = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("${username}:$password"))

$wsmanShellId = [Guid]::NewGuid().Guid
$body = '<s:Envelope xmlns:rsp="http://schemas.microsoft.com/wbem/wsman/1/windows/shell" xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:wsman="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:wsmv="http://schemas.microsoft.com/wbem/wsman/1/wsman.xsd"><s:Header><wsa:Action s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/09/transfer/Create</wsa:Action><wsmv:DataLocale s:mustUnderstand="false" xml:lang="en-US" /><wsman:Locale s:mustUnderstand="false" xml:lang="en-US" /><wsman:MaxEnvelopeSize s:mustUnderstand="true">153600</wsman:MaxEnvelopeSize><wsa:MessageID>uuid:7A91FD5B-6F0F-45FC-AB9F-A54E0951A4DD</wsa:MessageID><wsman:OperationTimeout>PT20S</wsman:OperationTimeout><wsa:ReplyTo><wsa:Address s:mustUnderstand="true">http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address></wsa:ReplyTo><wsman:ResourceURI s:mustUnderstand="true">http://schemas.microsoft.com/powershell/Microsoft.PowerShell</wsman:ResourceURI><wsmv:SessionId s:mustUnderstand="false">uuid:DBC75D41-FD02-414B-AA5D-EF7738E67DC5</wsmv:SessionId><wsa:To>https://127.0.0.1:5986/wsman</wsa:To><wsman:OptionSet s:mustUnderstand="true"><wsman:Option MustComply="true" Name="protocolversion">2.3</wsman:Option></wsman:OptionSet></s:Header><s:Body><rsp:Shell ShellId="{0}"><rsp:InputStreams>stdin pr</rsp:InputStreams><rsp:OutputStreams>stdout</rsp:OutputStreams><creationXml xmlns="http://schemas.microsoft.com/powershell">AAAAAAAAAAEAAAAAAAAAAAMAAADHAgAAAAIAAQAFMzUtlcVHFb9QH0Sf8fybAAAAAAAAAAAAAAAAAAAAADxPYmogUmVmSWQ9IjAiPjxNUz48VmVyc2lvbiBOPSJwcm90b2NvbHZlcnNpb24iPjIuMzwvVmVyc2lvbj48VmVyc2lvbiBOPSJQU1ZlcnNpb24iPjIuMDwvVmVyc2lvbj48VmVyc2lvbiBOPSJTZXJpYWxpemF0aW9uVmVyc2lvbiI+MS4xLjAuMTwvVmVyc2lvbj48L01TPjwvT2JqPgAAAAAAAAACAAAAAAAAAAADAAAC/QIAAAAEAAEABTM1LZXFRxW/UB9En/H8mwAAAAAAAAAAAAAAAAAAAAA8T2JqIFJlZklkPSIwIj48TVM+PEkzMiBOPSJNaW5SdW5zcGFjZXMiPjE8L0kzMj48STMyIE49Ik1heFJ1bnNwYWNlcyI+MTwvSTMyPjxPYmogTj0iUFNUaHJlYWRPcHRpb25zIiBSZWZJZD0iMSI+PFROIFJlZklkPSIwIj48VD5TeXN0ZW0uTWFuYWdlbWVudC5BdXRvbWF0aW9uLlJ1bnNwYWNlcy5QU1RocmVhZE9wdGlvbnM8L1Q+PFQ+U3lzdGVtLkVudW08L1Q+PFQ+U3lzdGVtLlZhbHVlVHlwZTwvVD48VD5TeXN0ZW0uT2JqZWN0PC9UPjwvVE4+PFRvU3RyaW5nPkRlZmF1bHQ8L1RvU3RyaW5nPjxJMzI+MDwvSTMyPjwvT2JqPjxPYmogTj0iQXBhcnRtZW50U3RhdGUiIFJlZklkPSIyIj48VE4gUmVmSWQ9IjEiPjxUPlN5c3RlbS5NYW5hZ2VtZW50LkF1dG9tYXRpb24uUnVuc3BhY2VzLkFwYXJ0bWVudFN0YXRlPC9UPjxUPlN5c3RlbS5FbnVtPC9UPjxUPlN5c3RlbS5WYWx1ZVR5cGU8L1Q+PFQ+U3lzdGVtLk9iamVjdDwvVD48L1ROPjxUb1N0cmluZz5VTktOT1dOPC9Ub1N0cmluZz48STMyPjI8L0kzMj48L09iaj48T2JqIE49Ikhvc3RJbmZvIiBSZWZJZD0iMyI+PE1TPjxCIE49Il9pc0hvc3ROdWxsIj50cnVlPC9CPjxCIE49Il9pc0hvc3RVSU51bGwiPnRydWU8L0I+PEIgTj0iX2lzSG9zdFJhd1VJTnVsbCI+dHJ1ZTwvQj48QiBOPSJfdXNlUnVuc3BhY2VIb3N0Ij50cnVlPC9CPjwvTVM+PC9PYmo+PE5pbCBOPSJBcHBsaWNhdGlvbkFyZ3VtZW50cyIgLz48L01TPjwvT2JqPg==</creationXml></rsp:Shell></s:Body></s:Envelope>' -f $wsmanShellId

$params = @{
    AllowUnencryptedAuthentication = $true
    Uri = 'http://server2022.domain.test:5985/wsman'
    Method = 'POST'
    ContentType = 'application/soap+xml;charset=UTF-8'
    Headers = @{
        'Accept-Encoding' = 'identity'
        Authorization = "Basic $basicString"
    }
    Body = $body
}

Measure-Command {
    Invoke-WebRequest @params | Out-Host
}

It requires you to enable Basic auth and AllowUnencrypted on the server side. On my Linux host it is about 170 milliseconds.