Skip to content

Commit 9efa79e

Browse files
committed
socket_dist: add listen/2 with dist_opts map
Add listen/2 accepting a generic options map, making the interface transport-agnostic. Port range iteration (dist_listen_min/dist_listen_max) is handled inside socket_dist rather than net_kernel. net_kernel passes dist_opts opaquely to ProtoDist:listen/2, so no net_kernel changes are needed when adding future dist module options. Usage: net_kernel:start(mynode, #{ name_domain => shortnames, dist_opts => #{dist_listen_min => 9100, dist_listen_max => 9110} }). Signed-off-by: Peter M <petermm@gmail.com>
1 parent 6f66fa4 commit 9efa79e

File tree

4 files changed

+77
-6
lines changed

4 files changed

+77
-6
lines changed

doc/src/distributed-erlang.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@ ok = net_kernel:set_cookie(<<"AtomVM">>).
3232

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

35+
### Distribution options
36+
37+
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. The built-in `socket_dist` module supports the following `dist_opts`:
38+
39+
- `dist_listen_min` — minimum port number to listen on
40+
- `dist_listen_max` — maximum port number to listen on
41+
42+
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).
43+
44+
```erlang
45+
{ok, _NetKernelPid} = net_kernel:start(mynode, #{
46+
name_domain => shortnames,
47+
dist_opts => #{dist_listen_min => 9100, dist_listen_max => 9110}
48+
}).
49+
```
50+
51+
When `dist_opts` is omitted or the port keys are not set, the OS assigns an ephemeral port (the default behaviour).
52+
3553
## `epmd`
3654

3755
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.

libs/estdlib/src/net_kernel.erl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
%% @param Options options for distribution. Supported options are:
7474
%% - `name_domain' : whether name should be short or long
7575
%% - `proto_dist' : the module used for distribution (e.g. `socket_dist')
76+
%% - `dist_opts' : a map of options passed through to the dist module's `listen/2'
7677
%%-----------------------------------------------------------------------------
7778
-spec start(atom(), map()) -> {ok, pid()}.
7879
start(Name, Options0) when is_atom(Name) andalso is_map(Options0) ->
@@ -81,6 +82,7 @@ start(Name, Options0) when is_atom(Name) andalso is_map(Options0) ->
8182
case Key of
8283
name_domain when Val =:= shortnames orelse Val =:= longnames -> ok;
8384
proto_dist when is_atom(Val) -> ok;
85+
dist_opts when is_map(Val) -> ok;
8486
_ -> error({invalid_option, Key, Val}, [Name, Options0])
8587
end
8688
end,
@@ -189,13 +191,14 @@ init(Options) ->
189191
process_flag(trap_exit, true),
190192
LongNames = maps:get(name_domain, Options, longnames) =:= longnames,
191193
ProtoDist = maps:get(proto_dist, Options, socket_dist),
194+
DistOpts = maps:get(dist_opts, Options, #{}),
192195
Name = maps:get(name, Options),
193196
Node = maps:get(node, Options),
194197
Cookie = crypto:strong_rand_bytes(16),
195198
TickInterval = (?NET_TICK_TIME * 1000) div ?NET_TICK_INTENSITY,
196199
Self = self(),
197200
Ticker = spawn_link(fun() -> ticker(Self, TickInterval) end),
198-
case ProtoDist:listen(Name) of
201+
case ProtoDist:listen(Name, DistOpts) of
199202
{ok, {Listen, _Address, Creation}} ->
200203
true = erlang:setnode(Node, Creation),
201204
AcceptPid = ProtoDist:accept(Listen),

libs/estdlib/src/socket_dist.erl

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
% dist interface
2323
-export([
2424
listen/1,
25+
listen/2,
2526
accept/1,
2627
accept_connection/5,
2728
setup/5,
@@ -37,12 +38,32 @@
3738

3839
-spec listen(string()) -> {ok, {any(), #net_address{}, pos_integer()}} | {error, any()}.
3940
listen(Name) ->
41+
listen(Name, #{}).
42+
43+
-spec listen(string(), map()) ->
44+
{ok, {any(), #net_address{}, pos_integer()}} | {error, any()}.
45+
listen(Name, Opts) ->
46+
PortMin = maps:get(dist_listen_min, Opts, 0),
47+
PortMax = maps:get(dist_listen_max, Opts, 0),
48+
validate_port_range(PortMin, PortMax),
49+
try_listen_port(Name, PortMin, PortMax).
50+
51+
validate_port_range(0, 0) -> ok;
52+
validate_port_range(Min, Max) when is_integer(Min), is_integer(Max), Min =< Max -> ok;
53+
validate_port_range(Min, Max) -> error({invalid_port_range, Min, Max}).
54+
55+
try_listen_port(_Name, Port, PortMax) when Port > PortMax ->
56+
{error, no_port_available};
57+
try_listen_port(Name, Port, PortMax) ->
58+
case do_listen(Name, Port) of
59+
{ok, _} = Ok -> Ok;
60+
{error, _} when Port < PortMax -> try_listen_port(Name, Port + 1, PortMax);
61+
{error, _} = Error -> Error
62+
end.
63+
64+
do_listen(Name, SocketPort) ->
4065
{ok, LSock} = socket:open(inet, stream, tcp),
41-
ok = socket:bind(LSock, #{
42-
family => inet,
43-
port => 0,
44-
addr => {0, 0, 0, 0}
45-
}),
66+
ok = socket:bind(LSock, #{family => inet, port => SocketPort, addr => {0, 0, 0, 0}}),
4667
ok = socket:listen(LSock),
4768
{ok, #{addr := Addr, port := Port}} = socket:sockname(LSock),
4869
ErlEpmd = net_kernel:epmd_module(),

tests/libs/estdlib/test_net_kernel.erl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ test() ->
4545
ok = test_link_local_unlink_remote(Platform),
4646
ok = test_link_local_unlink_local(Platform),
4747
ok = test_is_alive(Platform),
48+
ok = test_ping_with_dist_opts(Platform),
4849
ok;
4950
false ->
5051
io:format("~s: skipped\n", [?MODULE]),
@@ -483,6 +484,34 @@ test_is_alive(Platform) ->
483484
false = is_alive(),
484485
ok.
485486

487+
test_ping_with_dist_opts("BEAM") ->
488+
ok;
489+
test_ping_with_dist_opts("ATOM" = Platform) ->
490+
DistListenMin = 9100,
491+
DistListenMax = 9110,
492+
{ok, _NetKernelPid} = net_kernel:start(atomvm, #{
493+
name_domain => shortnames,
494+
dist_opts => #{dist_listen_min => DistListenMin, dist_listen_max => DistListenMax}
495+
}),
496+
Node = node(),
497+
erlang:set_cookie(Node, 'AtomVM'),
498+
%% Verify the listening port is within the requested range
499+
[Name, Host] = string:split(atom_to_list(Node), "@"),
500+
{ok, HostAddr} = inet:getaddr(Host, inet),
501+
{port, Port, _Version} = erl_epmd:port_please(Name, HostAddr),
502+
true = Port >= DistListenMin,
503+
true = Port =< DistListenMax,
504+
%% Verify connectivity still works
505+
Result = execute_command(
506+
Platform,
507+
"erl -sname " ++ ?OTP_SNAME ++ " -setcookie AtomVM -eval \"R = net_adm:ping('" ++
508+
atom_to_list(Node) ++
509+
"'), erlang:display(R).\" -s init stop -noshell"
510+
),
511+
"pong" ++ _ = Result,
512+
net_kernel:stop(),
513+
ok.
514+
486515
subprocess("BEAM", Command) ->
487516
open_port({spawn_executable, "/bin/sh"}, [{args, ["-c", Command]}, {line, 256}, eof, in]);
488517
subprocess("ATOM", Command) ->

0 commit comments

Comments
 (0)