Introducing VintageNet
- a new networking library for embedded Elixir devices,
specially designed for Nerves ππ»πΆ
https://github.com/nerves-networking/vintage_net
Why?
Good question.
Truth be told, I wasn’t directly involved in the struggles of nerves_network
or building vintage_net
, but am on the advocating end. So I think this can be
better explained from this blurb in the vintage_net
README.md by those who
built it:
VintageNet
takes a different approach to networking fromnerves_network
. It takes human-readable network configuration descriptions and transforms them into low level commands to run, supervisors to start, and routes to add. It supports calling traditional Linux utilities likeip
andudhcpc
to configure networks. This can be a timesaver for migrating complicated, but known working Linux setups to Nerves. Linux daemons are supervised byVintageNet
so they’re restarted if they crash and stopped when no longer needed. Of course, implementing services in pure Elixir also has advantages and you can replace the C implementations when and if you desire.Another important difference from
nerves_network
is thatVintageNet
doesn’t attempt to make incremental modifications to configurations. It completely tears down an interface’s connection and then brings up new configurations in a fresh state. Network reconfiguration is assumed to be an infrequent event so while this causes hiccups in network connectivity, it removes the complicated state machine code that madenerves_network
hard to maintain and extend.
noice.
Also, it is technically more directed towards Nerves, but extensible for running in other environments if needed. If you’re looking to go down that path, check out the system requirements for what you need to get running.
Depending on who you talk to, the answer to why? could really get in depth. If that tickles your fancy, then feel free to reach out for more discussion in the #nerves slack channel or Elixir forum. However, for this article I want to focus on some of the new hotness that makes it a great library to use for your embedded networking.
The Bells & Whistles
Mm, yes. The good stuff.
This isn’t going to be an exhaustive list but will hopefully shed light on being able to do more than connect to a WiFi network. I’ve broken it down to a few sections and linked below so you don’t have to take it all in at once:
- Extensible
- Runtime Configuration
- Persisted
- Access-Point Mode Support
- Network Event Subscriptions
- WiFi Network Prioritization
- Bonus - Nerves Pack
π»
Extensible
As stated earlier, vintage_net
is based on “old school” linux utilities and
designed so that you can replace most of those utilities with your own should
you so desire. Have your own implementation of ip
? Use it. Have a different
ifup
/ifdown
requirement? Ya, you can change that out. You wrote a custom
Unix domain socket for C to Elixir communication? Drop it in!
In fact, you can also even change what host that is pinged to check that internet is actually up which is useful if, say, you only consider network “up” if it can talk to your special server because the rest of the internet is dead to you.
If this is your jam, check out the Configuration documentation.
VintageNet
is also setup to support multiple technologies like
WiFi
,
Ethernet
, and
Direct
(i.e things like USB Gadget
)
right out of the box.
The design is modular and separated into their own libraries so you can include
only what you need by adding it as a dependency:
def deps() do
{:vintage_net, "~> 0.7"},
{:vintage_net_direct, "~> 0.7"},
{:vintage_net_ethernet, "~> 0.7"},
{:vintage_net_wifi, "~> 0.7"}
end
And if you need to support a new interface, just implement the
VintageNet.Technology
behaviour and drop it right in with the rest. Check out the
VintageNet.Technology
documentation
for more info.
Runtime Configuration
If you tried this with nerves_network
, then this might bring tears of joy to
your eyes. (hint: it was painful)
VintageNet
still supports compile time configuration, but in some cases one
may not want to store sensitive values in a config. For example, WiFi
credentials probably don’t need to be stored in the clear. Or say you’re
deploying to 1000 different devices that all connecting to different networks.
You may not want to store all 1000 network credentials in the config on every
device.
So, runtime it is!
Example:
config = %{
ipv4: %{method: :dhcp},
type: VintageNetWiFi,
vintage_net_wifi: %{
networks: [
%{key_mgmt: :wpa_psk, ssid: "sesame", psk: "open-sesame!"}
]
}
}
VintageNet.configure("wlan0", config)
:ok
π dust your hands off, cause that’s it.
One big thing to note here is that configuration is a total replacement and doesn’t add to existing configuration. The code above is the final config result. If you need to add to your config, simply fetch it first, add your network, then apply:
new_network = %{key_mgmt: :wpa_psk, ssid: "bullpen", psk: "peralta-ultimate-human-slash-genius"}
config = VintageNet.get_configuration("wlan0")
|> update_in([:vintage_net_wifi, :networks], & [new_network | &1])
VintageNet.configure("wlan0", config)
See Network Interface Configuration for more deets.
Persisted
Simply put, when you configure network settings, they are persisted to disk. So add that WiFi network with the stupid long password one time, then “fuggedaboutit”.
This is especially useful during firmware updates so that you still have the
network configured after updating the code. On start, VintageNet
also reads
the persisted configuration so you could pass around this configured file with
encrypted PSK so its there when needed, like during manufacturing.
Or, you can disable it entirely to prevent persistence cause you don’t need it.
VintageNet
has your back.
Persistence docs is the place to be for this.
Access-Point Mode Support
Sometimes you may want a device to broadcast a network instead of connect to a
network, or in other words, turn it into and Access Point. Well VintageNet
has
you covered! There is a bit of configuration, but it would look something like
this:
ap_config =
%{
dhcpd: %{
end: {192, 168, 0, 254},
max_leases: 235,
options: %{
dns: [{192, 168, 0, 1}],
domain: "nerves.local",
router: [{192, 168, 0, 1}],
search: ["nerves.local"],
subnet: {255, 255, 255, 0}
},
start: {192, 168, 0, 20}
},
dnsd: %{records: [{"nerves.local", {192, 168, 0, 1}}]},
ipv4: %{address: {192, 168, 0, 1}, method: :static, prefix_length: 24},
type: VintageNetWiFi,
vintage_net_wifi: %{networks: [%{key_mgmt: :none, mode: :ap, ssid: "connect-to-me!"}]}
}
VintageNet.configure("wlan0", ap_config)
There’s a lot there, but the break down is device gets configured with
mode: :ap
, sets a static IP of 192.168.0.1
, sets up DHCP so it can give
out IP’s, and uses dnsd
so that hostname can resolve to the static IP.
This specific example is used with
VintageNetWizard
which is a tool for using your a web page to configure networks on your device.
Check out the repo or our other write up -> Wizards &
WiFi
But using as a setup wizard is not the only use case. I, for example, will use it frequently on the go so I can still connect to a device when no cable or joint network is available.
Network Event Subscriptions
This is really cool π
VintageNet
keeps a key/value store of network information that can be queried
at any time. So say you want to know what the state of your ethernet connection
is? easy
iex)> VintageNet.get(["interface", "eth0", "connection"])
:internet
Or maybe your device is in AP mode and you want to see what clients are connect? Done
iex)> VintageNet.get(["interface", "wlan0", "wifi", "clients"])
["8c:86:1e:b6:e5:ba"]
Not only can you fetch, but you can also subscribe to the properties to get notified when they happen. Continuing with this example, what if I want to do some action only when a specific client connects? Well a simple GenServer ‘ought to do the trick Β¬
defmodule MyApp do
use GenServer
@subscription ["interface", "wlan0", "wifi", "clients"]
@special_client "8c:86:1e:b6:e5:ba"
def start_link(_) do
GenServer.start_link(__MODULE__, [])
end
def init(_) do
VintageNet.get(@subscription)
{:ok, %{}}
end
def handle_info({VintageNet, @subscription, left, joined, %{}}, state) do
cond do
@special_client in joined ->
# client joined. Sing and dance! or something else...
@special_client in left ->
# the client left the network, tell someone?
true ->
# not special so do whatever
end
end
end
Or maybe you want to perform actions when connections go up or down. In any case, Properties is where you want to look for more ideas.
WiFi Network Prioritization
Not only does VintageNet
allow you to configure multiple networks at once, but
you can set priorities for them which is helpful when you might be in an area
with multiple networks available, but you prefer network A over the others. Or
you prefer the 5 Ghz network over 2.4 Ghz, etc etc. An example might look like¬
config = %{
ipv4: %{method: :dhcp},
type: VintageNetWiFi,
vintage_net_wifi: %{
networks: [
%{key_mgmt: :wpa_psk, ssid: "sesame", psk: "open-sesame!", priority: 90},
%{key_mgmt: :wpa_psk, ssid: "bullpen", psk: "peralta-ultimate-human-slash-genius", priority: 100}
]
}
}
VintageNet.configure("wlan0", config)
Note: If you’re not familiar with wpa_supplicant
priority values, the
higher value is higher priority (vs a ranking system where 1 == highest)
Bonus - Nerves Pack
A common desire practice in Nerves and the embedded Elixir space is to try to
keep pieces modular because not every case needs every piece. It allows you to
pull in only what you need and keep firmware images small. VintageNet
is no
different here and to get a fully working user-end setup (SSH, mDNS, wifi wizard
configuration, etc) might require more pieces. For the more advance use, ndb.
For new to Nerves/Embedded, it could be a hurdle.
In the past, this was aided with nerves_init_gadget
which was a compilation of
all the typical pieces needed to make a firmware, install, and get to an iex
prompt within a few minutes. However, VintageNet
is totally incompatible with
nerves_init_gadget
… so I created a new one called NervesPack
! π
By adding nerves_pack
as a dependency, you’ll get all these benefits plus
pre-configured SSH, SFTP, mDNS, and all supported interfaces for your device
will automatically be configured at runtime.
Checkout the NervesPack
repo for
more info.