NIOSSH ChannelError when running from a daemon

Issue

When using the nio-ssh library to execute ssh commands in a daemonized context (built executable launched using launchctl with a config in /Library/LaunchDaemons) a ChannelError (operationUnsupported) is thrown.

I'm unsure if this is a problem just with nio-ssh or nio in general. Could it be that certain network operations aren't permitted from within a daemon?

Any information/help on this matter is greatly appreciated!

Related issue in the nio-ssh repository: https://github.com/apple/swift-nio-ssh/issues/166

Unfortunately there are no specific tags for these libraries (nio, nio-ssh) or for daemons, so I have used the Network tag instead.

Reproduction

Reproduction can be found here: https://github.com/eliaSchenker/nio-ssh-daemon-issue/tree/main

To run the reproduction follow these steps:

  • Build using Xcode (Product > Build)
  • Find the executable in the build folder (Product > Show Build Folder in Finder)
  • Move the executable to /Library/PrivilegedHelperTools
  • Create a daemon configuration in /Library/LaunchDaemons/nio-ssh-daemon.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
	<dict>
		<key>Label</key>
		<string>nio-ssh-daemon</string>
		<key>ProgramArguments</key>
         <array>
             <string>/Library/PrivilegedHelperTools/nio-ssh-daemon</string>
	    	 <string>username:password@host</string>
			 <string>ls -la</string>
         </array>
		<key>KeepAlive</key>
		<true/>
		<key>ProcessType</key>
		<string>Interactive</string>
		<key>StandardOutPath</key>
		<string>/Library/Logs/nio-ssh-daemon.out.log</string>
		<key>StandardErrorPath</key>
		<string>/Library/Logs/nio-ssh-daemon.err.log</string>
	</dict>
</plist>

making sure to adjust the program arguments to include an host with username and password.

  • Load the daemon using
sudo launchctl load nio-ssh-daemon.plist
  • When opening Console.app, navigating to Log Reports and opening nio-ssh-daemon.out.log the logged error will be shown:
Creating bootstrap
Connecting channel
Creating child channel
Waiting for connection to close
Error in pipeline: operationUnsupported
An error occurred: commandExecFailed

If the executable is run manually without a daemon it will work correctly:

./nio.ssh-daemon username:password@host

The reproduction is a copy of the example in the repository (https://github.com/apple/swift-nio-ssh/tree/main/Sources/NIOSSHClient) with slight modifications to log errors instead of using try!.

Accepted Reply

With some help from a contributor (Lukasa) I was able to adjust the example to work on a daemon. The problem was that the example binds itself to the stdout (writing the response of the ssh command to it). As the stdout is bound to a file in a daemon and the bootstrap includes a check that the output is not a file, an error would occur.

For anyone else experiencing this issue in the future, I've updated the reproduction to include the solution: https://github.com/eliaSchenker/nio-ssh-daemon-issue/tree/main

Replies

Could it be that certain network operations aren't permitted from within a daemon?

That’s possible, but unlikely. In general, a daemon has free access to the network.

I think you’ll need to trace through the NIO code to find out which Apple API is failing.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

After some further debugging I found that the thrown error happens in Bootstrap.swift in the method validateFileDescriptorIsNotAFile. This method is called by _takingOwnershipOfDescriptors (same file). The input and output for the ownership are 0 and 1, the validation for the input passes, the one for the output fails at the following line:

  private func validateFileDescriptorIsNotAFile(_ descriptor: CInt) throws {
        // ...
        var s: stat = .init()
        try withUnsafeMutablePointer(to: &s) { ptr in
            try Posix.fstat(descriptor: descriptor, outStat: ptr)
        }
        switch s.st_mode & S_IFMT {
        case S_IFREG, S_IFDIR, S_IFLNK, S_IFBLK:
            throw ChannelError.operationUnsupported // Here the error occurrs
        default:
            () // Let's default to ok
        }

When running the application in the agent, the st_mode is 33188, when running it normally in Xcode, it is 8592.

With some help from a contributor (Lukasa) I was able to adjust the example to work on a daemon. The problem was that the example binds itself to the stdout (writing the response of the ssh command to it). As the stdout is bound to a file in a daemon and the bootstrap includes a check that the output is not a file, an error would occur.

For anyone else experiencing this issue in the future, I've updated the reproduction to include the solution: https://github.com/eliaSchenker/nio-ssh-daemon-issue/tree/main