3

I am connecting to a Unix server using SSH.NET with the following code

ssh = new SshClient("myserver.univ.edu", Username, Password);
ssh.Connect();

The connection goes through and appears to generate no exception. The server is set up to require two factor authentication, but I get no prompt on my phone (using that as physical authenticator/OTP device). But the connection seems to be ok.

I then issue a command with this code:

SshCommand NewLookup = ssh.CreateCommand("newlookup " + IpNameOrAddress.Text))
LogText.Text += NewLookup.Execute().Replace("\n", Environment.NewLine);

And then I get the push to my phone (2nd factor verification request). Once I accept the verification request through the phone, then the command executes just fine. This would be all ok, except that...

Problem

I get a push to my phone for every subsequent command, so if I want to use that connection to run multiple commands, I have to sit on my phone clicking "Accept" for every command. So, how do I avoid getting a push on every command?

5
  • What is the idle time? How long between command? Commented Jul 20, 2018 at 20:18
  • 1
    @Jeremiah - a few seconds, less than 5, and I have increased the connection timeout to 60 seconds. Nice thought though. Commented Jul 20, 2018 at 20:18
  • I just increased the connection timeout to 15 minutes with no effect. Commented Jul 20, 2018 at 20:27
  • 1
    The issue is you are using the command channel in ssh to send commands, which is not like a shell session. While not normally recommended, I think you need to look into sending commands with a ShellStream instead. Commented Jul 20, 2018 at 23:27
  • @NetMage - yes, that's what I was thinking, but with all the warnings about ShellStream not being recommended, I was reluctant to go there. Please put that as an answer and I will accept it if I don't get anything else soon. Commented Jul 23, 2018 at 12:42

1 Answer 1

4
+250

In order to send multiple commands in a single session with SSH.NET, you probably need to use a ShellStream. This should reduce your 2FA approval to just the opening of the session to the host. This can also be useful for devices (such as HPE Switches) that don't support a command channel, but do support SSH remote terminal (e.g. you can putty to them), and for situations where commands change the (shell) environment and so you need to keep the session open for the duration of the job. Otherwise, the SSH command channel is the intended (and better) way to handle this.

You can find further SSH.NET documentation at NuDoc - SSH.NET and the GitHub Releases for the SSH.Net project include a Windows Help file.

Here's some code I wrote to wrap the ShellStream in another object that keeps a StreamReader and StreamWriter and handles reading input from a(n HP) switch and filtering out the escape sequences, as well as reading up to the next prompt:

public static class SshClientExt {
    public static ExtShellStream CreateExtShellStream(this SshClient sc, string termName, uint rows, uint cols, uint width, uint height, int bufSize) =>
        new ExtShellStream(sc.CreateShellStream(termName, rows, cols, width, height, bufSize));
}

public class ExtShellStream : IDisposable {
    static Regex reEscVT100 = new Regex("\x1B\\[[^A-Z]+[A-Z]", RegexOptions.Compiled);
    static TimeSpan ReadTimeout = new TimeSpan(0, 0, 10);

    ShellStream ssh;
    StreamReader sr;
    StreamWriter sw;

    public ExtShellStream(ShellStream anSSH) {
        ssh = anSSH;
        sr = new StreamReader(ssh);
        sw = new StreamWriter(ssh);
    }

    public List<string> ReadLines() {
        // try to read all in
        long prev;
        do {
            prev = ssh.Length;
            Thread.Sleep(250);
        } while (ssh.Length != prev);

        "-".Repeat(40).Dump();
        var ans = new List<string>();

        while (true) {
            var line = sr.ReadLine();
            if (line != null) {
                line = line.Remove(reEscVT100).TrimEnd();
                $@"""{line}""".Dump();
                if (line.EndsWith("#")) // stop when prompt appears
                    break;
                else
                    ans.Add(line);
            }
            else
                Thread.Sleep(500);
        }

        return ans;
    }

    public void DumpLines() => ReadLines();

    public List<string> DoCommand(string cmd) {
        sw.Write(cmd);
        sw.Write("\r");
        sw.Flush();
        while (!ssh.DataAvailable)
            Thread.Sleep(500);
        return ReadLines().SkipWhile(l => l == cmd).ToList();
    }

    #region IDisposable Support
    private bool disposedValue = false; // To detect redundant calls

    protected virtual void Dispose(bool disposing) {
        if (!disposedValue) {
            if (disposing) {
                // prevent double dispose
                // don't dispose of sr or sw: only disposable resource is ssh
                ssh.Dispose();
            }

            disposedValue = true;
        }
    }

    // This code added to correctly implement the disposable pattern.
    public void Dispose() {
        // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
        Dispose(true);
    }
    #endregion

}

And here's a sample function that uses that code with SSH.Net to retrieve ons-screen copies of configuration information from the switch:

public static void RetrieveConfigFiles(IDFStack idf) {
    using (var sshClient = new SshClient(idf.IPAddress, username, password)) {
        sshClient.Connect();

        using (var ssh = sshClient.CreateExtShellStream("dumb", 120, 80, 0, 0, 200000)) {
            ssh.DumpLines();
            ssh.DoCommand("no page");

            File.WriteAllLines(idf.ConfigPath, ssh.DoCommand("show running-config structured"));
            File.WriteAllLines(idf.StatusPath, ssh.DoCommand("show interfaces brief"));
            File.WriteAllLines(idf.LLDPPath, ssh.DoCommand("show lldp info remote-device detail"));
        }

        sshClient.Disconnect();
    }
}
Sign up to request clarification or add additional context in comments.

9 Comments

You could have just said "Use a shellstream", but you went above and beyond to give a very complete code example. Considering how little documentation and few examples for ssh.net are out there, this is gold. Can't thank you enough. I've set up a bounty and as soon as SE lets me, I'll award that to you.
@AgapwIesu Just FYI, the documentation for SSH.NET is hard to discover, but there is some nice API documentation at NuDoq - SSH.NET.
I should have found that! But I didn't... sigh (frustrated at myself).... thanks again for going the extra mile.
@MartinPrikryl Added some caveats/explanation for using this.
The implementation of 2FA on that host is as vanilla as it gets. The problem here is the "per-command authentication", which is a property of how SSH.NET implemented its SshClient.Connect (little more than an IP connection) and its CreateCommand (authentication plus execute command). If CreateCommand does the authentication, it does not matter how your 2FA is implemented, the user will go through it every time a CreateCommand is issued.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.