Starting off with a new Nerves Project -- Issues with SSH

I recently started a new Nerves application with my Raspberry Pi 3, using the mix nerves.new command. I believe it sets up with Nerves Pack. I have it connected to my home Wi-Fi network alongside my Mac.
I can access it through my display and a keyboard, plugged into a power supply, but I cannot ssh into it for the life of me, and I have no idea why.

My configurations are below:

Nerves firmware: built with
MIX_TARGET=rpi3 mix firmware

VintageNet config (in config.exs):

config :vintage_net, :config,
  {"wlan0", %{
     type: VintageNetWiFi,
     vintage_net_wifi: %{
       ssid: "MY_SSID",
       psk:  "MY_PASS"
     },
     ipv4: %{method: :dhcp},
     hostname: "nerves-3ff8",
     domains:  ["local"]
  }}

MdnsLite config (in the same target.exs):

config :mdns_lite,
  hosts: ["nerves"],
  ttl:   120,
  services: [
    %{protocol: "ssh",      transport: "tcp", port: 22},
    %{protocol: "sftp-ssh", transport: "tcp", port: 22},
    %{protocol: "epmd",     transport: "tcp", port: 4369}
  ]

What I’ve verified on the Nerves device

$ ip addr show wlan0
3: wlan0: <…> 
    inet <IP> brd <IP> scope global wlan0
$ weather
...shows local weather information

What I have tried

ssh nerves.local
ssh root@nerves.local
ssh nerves-3ff8
ssh root@nerves-3ff8
ping IP

and all of them return:
connect to host nerves-3ff8.local port 22: Undefined error: 0

I can ping from within the nerves iex however.

Can anyone point me towards a solution? Im assuming a network issue? but Im not quite sure.

What is the output of

dns-sd -B _ssh._tcp

This shows you all the mDNS entries for SSH. There you should see something like:

Browsing for _ssh._tcp
DATE: ---Sun 06 Jul 2025---
11:23:08.722  ...STARTING...
Timestamp     A/R    Flags  if Domain               Service Type         Instance Name
11:23:08.724  Add        2  41 local.               _ssh._tcp.           nerves-fe79

Although the device should already the answer from ping and you’ve already tried it with the IP address.

Maybe the ouput of

ssh -v IP

will shed some light on the issue

the output of dns-sd -B _ssh._tcp

➜ dns-sd -B _ssh._tcp
Browsing for _ssh._tcp
DATE: ---Sun 06 Jul 2025---
20:21:30.944  ...STARTING...
Timestamp     A/R    Flags  if Domain               Service Type         Instance Name
20:21:30.945  Add        2  11 local.               _ssh._tcp.           nerves-3ff

Also, I did similar:

➜ dns-sd -G v4 nerves-3ff8.local
DATE: ---Sun 06 Jul 2025---
20:22:45.101  ...STARTING...
Timestamp     A/R  Flags         IF  Hostname                       Address                TTL
20:22:45.102  Add  40000002      11  nerves-3ff8.local.           <REDACTED>               120

ssh -v IP just shows me the same issue of:

“connect to host IP port 22: No route to host”

Very sad

This really looks like a strange networking issue since mDNS works but going direct doesn’t. Sometimes people have their WiFi routers configured to not allow devices on the same network to communicate with each other, but that’s usually more common on public Wi-Fi.

I don’t have a good reason why this would work in your case, but for weird issues like this, I always like to try pre-built Nerves firmware so that I can reproduce locally if necessary. The RPi3 one at Release v0.13.1 · elixir-circuits/circuits_quickstart · GitHub would be the one that I’d pick. WiFi isn’t configured on it, but it sounds like you can get to a local IEx prompt, so run VintageNetWiFi.quick_configure("SSID", "password") on the first boot.

I made the following change in my config for nerves_ssh:


# before
config :nerves_ssh,
  authorized_keys: Enum.map(keys, &File.read!/1)

# after
config :nerves_ssh,
  port: 22,
  authorized_keys: Enum.map(keys, &File.read!/1)

By specifying the port in the nerves_ssh config, I was able to connect to it via ssh!

Is this something that is known? I did not touch the :nerves_ssh config when it was bootstrapped with mix nerves.new.

Seems like in the nerves_ssh package, the port config should default to 22, but it didn’t for me :thinking:

This is definitely not known. The default port is 22. I just tried to reproduce this on a Raspberry Pi 4 and couldn’t. If you have a chance, could you try repeating your steps from mix nerves.new to see if something else might have caused the problem?

Reproduce steps:

  1. created new project with mix nerves.new nervous
  2. set up MIX_TARGET with export MIX_TARGET=rpi3
  3. install deps with mix deps.get
  4. update vintage_net config, adding in my network info
  5. create firmware with mix firmware
  6. plug microSD card and burn with mix burn
  7. insert microSD into device, and plug into power source
  8. after a minute, ran ssh nerves.local
    connect to host IP port 22: No route to host

Next, connected the device into my display and checked both ping and weather (both worked)

Afterwards, I changed the config for :nerves_ssh again to:

config :nerves_ssh,
  port: 22,
  authorized_keys: Enum.map(keys, &File.read!/1)

ran

mix firmware
mix burn

reloaded the device with the sd card and plugged it into power.
retried ssh nerves.local

message shown about ssh key auth.
Nerves IEx session started.

Anything else I should try?

Can you run for port <- Port.list, do: {Port.info(port), :inet.sockname(port)} on the non-connectable system (or even both)?

sure, here is the output from my target device:

for port <- Port.list, do: {Port.info(port), :inet.sockname(port)}

[
  {[
     name: ~c"forker",
     links: [],
     id: 0,
     connected: #PID<0.0.0>,
     input: 0,
     output: 0,
     os_pid: :undefined
   ], {:error, :einval}},
  {[
     registered_name: :heart_port,
     name: ~c"heart -pid 76 -ht 30",
     links: [#PID<0.1057.0>],
     id: 16,
     connected: #PID<0.1057.0>,
     input: 1,
     output: 4,
     os_pid: 105
   ], {:error, :einval}},
  {[
     name: ~c"2/2",
     links: [#PID<0.1067.0>],
     id: 32,
     connected: #PID<0.1067.0>,
     input: 0,
     output: 0,
     os_pid: :undefined
   ], {:error, :einval}},
  {[
     name: ~c"/srv/erlang/lib/nerves_uevent-0.1.2/priv/uevent",
     links: [#PID<0.1136.0>],
     id: 80,
     connected: #PID<0.1136.0>,
     input: 52600,
     output: 0,
     os_pid: 109
   ], {:error, :einval}},
  {[
     name: ~c"/srv/erlang/lib/nerves_logging-0.2.3/priv/kmsg_tailer",
     links: [#PID<0.1152.0>],
     id: 144,
     connected: #PID<0.1152.0>,
     input: 22192,
     output: 0,
     os_pid: 121
   ], {:error, :einval}},
  {[
     name: ~c"udp_inet",
     links: [#PID<0.1153.0>],
     id: 160,
     connected: #PID<0.1153.0>,
     input: 0,
     output: 0,
     os_pid: :undefined
   ], {:ok, {:local, "/dev/log"}}},
  {[
     name: ~c"udp_inet",
     links: [#PID<0.1159.0>],
     id: 192,
     connected: #PID<0.1159.0>,
     input: 0,
     output: 0,
     os_pid: :undefined
   ], {:ok, {:local, "/tmp/nerves_time_comm"}}},
  {[
     name: ~c"udp_inet",
     links: [#PID<0.1182.0>],
     id: 208,
     connected: #PID<0.1182.0>,
     input: 0,
     output: 0,
     os_pid: :undefined
   ], {:ok, {:local, "/tmp/beam_notify-89749473"}}},
  {[
     name: ~c"/srv/erlang/lib/vintage_net-0.13.7/priv/if_monitor",
     links: [#PID<0.1183.0>],
     id: 224,
     connected: #PID<0.1183.0>,
     input: 3565,
     output: 0,
     os_pid: 126
   ], {:error, :einval}},
  {[
     name: ~c"/srv/erlang/lib/muontrap-1.6.1/priv/muontrap",
     links: [#PID<0.1227.0>],
     id: 448,
     connected: #PID<0.1227.0>,
     input: 0,
     output: 0,
     os_pid: 149
   ], {:error, :einval}},
  {[
     name: ~c"tcp_inet",
     links: [#PID<0.1256.0>],
     id: 528,
     connected: #PID<0.1256.0>,
     input: 0,
     output: 0,
     os_pid: :undefined
   ], {:ok, {{0, 0, 0, 0, 0, 0, 0, 0}, 22}}},
  {[
     name: ~c"/srv/erlang/lib/muontrap-1.6.1/priv/muontrap",
     links: [#PID<0.1266.0>],
     id: 608,
     connected: #PID<0.1266.0>,
     input: 335,
     output: 5,
     os_pid: 177
   ], {:error, :einval}},
  {[
     name: ~c"udp_inet",
     links: [#PID<0.1267.0>],
     id: 624,
     connected: #PID<0.1267.0>,
     input: 0,
     output: 2248,
     os_pid: :undefined
   ], {:ok, {:local, "/tmp/vintage_net/wpa_supplicant/wlan0.ex"}}},
  {[
     name: ~c"/srv/erlang/lib/muontrap-1.6.1/priv/muontrap",
     links: [#PID<0.1269.0>],
     id: 640,
     connected: #PID<0.1269.0>,
     input: 200,
     output: 4,
     os_pid: 182
   ], {:error, :einval}},
  {[
     name: ~c"tcp_inet",
     links: [#PID<0.1274.0>],
     id: 960,
     connected: #PID<0.1274.0>,
     input: 0,
     output: 4165,
     os_pid: :undefined
   ], {:ok, {{0, 0, 0, 0, 0, 65535, 49320, 2667}, 22}}}
]

Is this the one that works or doesn’t work? It looks like it works.

We might be at the point where if you have a firmware that consistently does not work, it would be useful if you could share it (privately, since I think you have WiFi credentials in it). We must not be asking the right questions since everything so far seems like it should work, and this is a pretty well-tested area or so I thought before your post.

The ssh works now when I specifically add the port 22 to my nerves_ssh configuration.

Sure! If you have a moment to try it yourself, I can share you my private repo and I can commit my target.exs. Or, if you have a more preferable method, I can do that as well.