Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions doc/src/distributed-erlang.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ ok = net_kernel:set_cookie(<<"AtomVM">>).

`net_kernel:stop/0` can be used to stop distribution.

### Distribution options

The options map passed to `net_kernel:start/2` supports a `dist_opts` key containing a map of options that are forwarded to the distribution module's `listen/2` function.

#### socket_dist options

The built-in `socket_dist` module supports the following `dist_opts`:

- `dist_listen_min` — minimum port number to listen on
- `dist_listen_max` — maximum port number to listen on

Both must be specified together. `socket_dist` will try each port in the range until one is available. This is useful on systems where only a specific range of ports is open (e.g. firewall rules on embedded devices).

```erlang
{ok, _NetKernelPid} = net_kernel:start(mynode, #{
name_domain => shortnames,
dist_opts => #{dist_listen_min => 9100, dist_listen_max => 9110}
}).
```

When `dist_opts` is omitted or the port keys are not set, the OS assigns an ephemeral port (the default behaviour).

## `epmd`

AtomVM nodes can use Erlang/OTP's epmd on Unix systems. AtomVM is also bundled with a pure Erlang implementation of `epmd` which can be used on all platforms. Module is called `epmd`, to be distinguished from `erl_epmd` which is the client.
Expand Down
5 changes: 4 additions & 1 deletion libs/estdlib/src/net_kernel.erl
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
%% @param Options options for distribution. Supported options are:
%% - `name_domain' : whether name should be short or long
%% - `proto_dist' : the module used for distribution (e.g. `socket_dist')
%% - `dist_opts' : a map of options passed through to the dist module's `listen/2'
%%-----------------------------------------------------------------------------
-spec start(atom(), map()) -> {ok, pid()}.
start(Name, Options0) when is_atom(Name) andalso is_map(Options0) ->
Expand All @@ -81,6 +82,7 @@ start(Name, Options0) when is_atom(Name) andalso is_map(Options0) ->
case Key of
name_domain when Val =:= shortnames orelse Val =:= longnames -> ok;
proto_dist when is_atom(Val) -> ok;
dist_opts when is_map(Val) -> ok;
_ -> error({invalid_option, Key, Val}, [Name, Options0])
end
end,
Expand Down Expand Up @@ -189,13 +191,14 @@ init(Options) ->
process_flag(trap_exit, true),
LongNames = maps:get(name_domain, Options, longnames) =:= longnames,
ProtoDist = maps:get(proto_dist, Options, socket_dist),
DistOpts = maps:get(dist_opts, Options, #{}),
Name = maps:get(name, Options),
Node = maps:get(node, Options),
Cookie = crypto:strong_rand_bytes(16),
TickInterval = (?NET_TICK_TIME * 1000) div ?NET_TICK_INTENSITY,
Self = self(),
Ticker = spawn_link(fun() -> ticker(Self, TickInterval) end),
case ProtoDist:listen(Name) of
case ProtoDist:listen(Name, DistOpts) of
{ok, {Listen, _Address, Creation}} ->
true = erlang:setnode(Node, Creation),
AcceptPid = ProtoDist:accept(Listen),
Expand Down
31 changes: 26 additions & 5 deletions libs/estdlib/src/socket_dist.erl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
% dist interface
-export([
listen/1,
listen/2,
accept/1,
accept_connection/5,
setup/5,
Expand All @@ -37,12 +38,32 @@

-spec listen(string()) -> {ok, {any(), #net_address{}, pos_integer()}} | {error, any()}.
listen(Name) ->
listen(Name, #{}).

-spec listen(string(), map()) ->
{ok, {any(), #net_address{}, pos_integer()}} | {error, any()}.
listen(Name, Opts) ->
PortMin = maps:get(dist_listen_min, Opts, 0),
PortMax = maps:get(dist_listen_max, Opts, 0),
validate_port_range(PortMin, PortMax),
try_listen_port(Name, PortMin, PortMax).

validate_port_range(0, 0) -> ok;
validate_port_range(Min, Max) when is_integer(Min), is_integer(Max), Min =< Max -> ok;
validate_port_range(Min, Max) -> error({invalid_port_range, Min, Max}).

try_listen_port(_Name, Port, PortMax) when Port > PortMax ->
{error, no_port_available};
try_listen_port(Name, Port, PortMax) ->
case do_listen(Name, Port) of
{ok, _} = Ok -> Ok;
{error, _} when Port < PortMax -> try_listen_port(Name, Port + 1, PortMax);
{error, _} = Error -> Error
end.

do_listen(Name, SocketPort) ->
{ok, LSock} = socket:open(inet, stream, tcp),
ok = socket:bind(LSock, #{
family => inet,
port => 0,
addr => {0, 0, 0, 0}
}),
ok = socket:bind(LSock, #{family => inet, port => SocketPort, addr => {0, 0, 0, 0}}),
ok = socket:listen(LSock),
{ok, #{addr := Addr, port := Port}} = socket:sockname(LSock),
ErlEpmd = net_kernel:epmd_module(),
Expand Down
29 changes: 29 additions & 0 deletions tests/libs/estdlib/test_net_kernel.erl
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ test() ->
ok = test_link_local_unlink_remote(Platform),
ok = test_link_local_unlink_local(Platform),
ok = test_is_alive(Platform),
ok = test_ping_with_dist_opts(Platform),
ok;
false ->
io:format("~s: skipped\n", [?MODULE]),
Expand Down Expand Up @@ -483,6 +484,34 @@ test_is_alive(Platform) ->
false = is_alive(),
ok.

test_ping_with_dist_opts("BEAM") ->
ok;
test_ping_with_dist_opts("ATOM" = Platform) ->
DistListenMin = 9100,
DistListenMax = 9110,
{ok, _NetKernelPid} = net_kernel:start(atomvm, #{
name_domain => shortnames,
dist_opts => #{dist_listen_min => DistListenMin, dist_listen_max => DistListenMax}
}),
Node = node(),
erlang:set_cookie(Node, 'AtomVM'),
%% Verify the listening port is within the requested range
[Name, Host] = string:split(atom_to_list(Node), "@"),
{ok, HostAddr} = inet:getaddr(Host, inet),
{port, Port, _Version} = erl_epmd:port_please(Name, HostAddr),
true = Port >= DistListenMin,
true = Port =< DistListenMax,
%% Verify connectivity still works
Result = execute_command(
Platform,
"erl -sname " ++ ?OTP_SNAME ++ " -setcookie AtomVM -eval \"R = net_adm:ping('" ++
atom_to_list(Node) ++
"'), erlang:display(R).\" -s init stop -noshell"
),
"pong" ++ _ = Result,
net_kernel:stop(),
ok.

subprocess("BEAM", Command) ->
open_port({spawn_executable, "/bin/sh"}, [{args, ["-c", Command]}, {line, 256}, eof, in]);
subprocess("ATOM", Command) ->
Expand Down
Loading