DEV Community

Nexus Shell
Nexus Shell

Posted on

Fixing macOS "No route to host" when an SSH app never asks for Local Network access

While testing an SSH app on newer macOS versions, I hit a confusing failure:

  • Connecting to public hosts worked.
  • Connecting to a private IP or a .local host returned No route to host.
  • macOS never showed the Local Network permission prompt.
  • The app did not appear under System Settings > Privacy & Security > Local Network.

The app already had NSLocalNetworkUsageDescription in its Info.plist. That string explains the permission, but it does not itself trigger the TCC check.

Why the prompt can be missed

The SSH connection in this app is handled by a spawned system ssh process. In some macOS environments, relying on traffic from that child process did not reliably make the parent app appear in the Local Network privacy list.

The practical fix was to let the app process itself briefly touch the local endpoint before starting the normal SSH flow.

import Network

func triggerLocalNetworkPermission(host: String, port: UInt16) {
    guard let nwPort = NWEndpoint.Port(rawValue: port) else { return }

    let connection = NWConnection(
        host: NWEndpoint.Host(host),
        port: nwPort,
        using: .tcp
    )

    connection.stateUpdateHandler = { state in
        switch state {
        case .ready, .failed:
            connection.cancel()
        default:
            break
        }
    }

    connection.start(queue: .global(qos: .utility))

    DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 3) {
        connection.cancel()
    }
}
Enter fullscreen mode Exit fullscreen mode

This connection is only a permission trigger. Its result should not decide whether SSH is allowed to continue; the real connection still owns success, authentication, host-key verification, and error handling.

Only probe local destinations

Do not create an extra connection for every host. I limit the trigger to destinations that can be identified locally:

  • RFC 1918 IPv4 ranges: 10/8, 172.16/12, 192.168/16
  • IPv4 link-local: 169.254/16
  • mDNS names ending in .local
  • IPv6 link-local and unique-local addresses

I also cache the host in memory so it is touched only once per app launch.

A few implementation details that matter

  1. Keep NSLocalNetworkUsageDescription; the in-process connection complements it rather than replacing it.
  2. Start the trigger before every real connection path converges, so terminal, file, and monitoring flows behave consistently.
  3. Cancel quickly. If the permission sheet is waiting for user input, the connection can remain in preparing or waiting, so a timeout is still needed.
  4. Avoid claiming that every No route to host error is a permission issue. Routing, VPNs, firewalls, and offline servers can produce the same message.

This came from a real fix in Nexus Shell v1.6.7, the native macOS SSH app I develop. The same pattern may help other macOS apps that delegate local-network work to child processes.

Top comments (0)