Closed Yannik closed 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
kinit username@DOMAIN.COM
and then Enter-PSSession
/Invoke-Command
without any explicit credentialThe 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.
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.
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.
Thank you, @jborean93!
Hey Jordan,
I hope you are well.
Is there perhaps any progress on your wsman client project? I'm still very much interested :)
Yannik
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.
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...?
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.
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.)
Let me explain exactly what I am doing. I am using these two commands:
Enter-PSSession -Credential (Get-Credential) dc1.domain.com -Authentication Kerberos
(takes about 9 seconds)cd ..
(takes about 2.5 seconds)I have done two things to debug this:
export KRB5_TRACE=/dev/stdout
Enter-PSSession
:
cd ..
:
Can you make any sense of this?
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.
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
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.
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:
docker run -it debian:bullseye /bin/bash
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