Finding routing interface in Erlang
I have a machine with multiple network interfaces each of which connects to a different network. I want to, from an Erlang application, find which interface will be used to conn开发者_JAVA技巧ect to a given host.
For example I have a machine with interfaces, eth0 and eth1. eth0 is on a 10.x.x.x network, eth1 is on a 192.168.0.x network. I want a function that give the ip address 10.0.1.2 will tell me eth0 and given the ip address 192.168.0.74 will tell me eth1.
While you can read the list of interfaces and their addresses and masks in standard (if undocumented) Erlang with inet:getifaddrs/0
, you can't get the list of routes which provides the other information you need to get the route for address. On most hosts (those that don't have a complicated routing setup as they generally don't forward packets) you usually just need the default route together with the interface addresses and masks in order to figure out how the host will route a packet for a given address.
To begin, you need a list of interfaces and their routes. The getifaddrs/0
function gives you a proplist by interface and a list of addresses and masks. Because interfaces can have multiple addresses assigned to them you need a little bit of list parsing:
routes() ->
{ok, IFData} = inet:getifaddrs(),
lists:append([ routes(IF, IFOpts) || {IF, IFOpts} <- IFData ]).
routes(IF, Opts) ->
{_,Routes} = lists:foldl(fun parse_opts/2, {undefined, []}, Opts),
[{IF, Route}
|| Route <- Routes].
parse_opts({addr, Addr}, {undefined, Routes}) ->
{{addr, Addr}, Routes};
parse_opts({netmask, Mask}, {{addr, Addr}, Routes})
when tuple_size(Mask) =:= tuple_size(Addr) ->
{undefined, [{Addr, Mask} | Routes]};
parse_opts(_, Acc) -> Acc.
Now that you have a list of route information Routes::[Route::{Interface::string(), {Addr::tuple, Mask::Tuple}}]
you can find the matching routes:
routes_for(Targ, Routes) ->
[ RT || RT = {_IF, {Addr, Mask}} <- Routes,
tuple_size(Targ) =:= tuple_size(Addr),
match_route(Targ, Addr, Mask)
].
To match a route you need to mask the target address and compare that to the masked interface address. Masking an address provides the network address.
match_route(Targ, Addr, Mask)
when tuple_size(Targ) =:= tuple_size(Addr),
tuple_size(Targ) =:= tuple_size(Mask) ->
lists:all(fun (A) -> A end,
[element(I, Targ) band element(I, Mask)
=:= element(I, Addr) band element(I, Mask)
|| I <- lists:seq(1, tuple_size(Targ)) ]).
You can then sort the routes in order of preference by comparing the number of high one bits in the mask. Handily, the bytes-of-mask-as-tuple format Erlang uses to represent masks compares directly as below:
sort_routes(Routes) ->
lists:sort(fun ({_, {_AddrA, MaskA}}, {_, {_AddrB, MaskB}}) ->
MaskA > MaskB
end,
Routes).
Now put it all together:
route(Targ) ->
route(Targ, routes()).
route(Targ, Routes) ->
sort_routes(routes_for(Targ, Routes)).
routes_for(Targ, Routes) ->
[ RT || RT = {_IF, {Addr, Mask}} <- Routes,
tuple_size(Targ) =:= tuple_size(Addr),
match_route(Targ, Addr, Mask)
].
On my machine right now I have the following routes:
[{"lo0",
{{0,0,0,0,0,0,0,1},
{65535,65535,65535,65535,65535,65535,65535,65535}}},
{"lo0",{{127,0,0,1},{255,0,0,0}}},
{"lo0",
{{65152,0,0,0,0,0,0,1},{65535,65535,65535,65535,0,0,0,0}}},
{"en0",{{192,168,1,7},{255,255,255,0}}},
{"en0",
{{65152,0,0,0,1548,52991,65242,57142},
{65535,65535,65535,65535,0,0,0,0}}},
{"vmnet1",{{172,16,0,1},{255,255,255,0}}},
{"vmnet8",{{192,168,148,1},{255,255,255,0}}}]
So when finding a route to 127.0.1.1 (an address in the 127.0.0.0/8 network), I get:
route({127,0,0,1}) -> [{"lo0",{{127,0,0,1},{255,0,0,0}}}]
. Unfortunately I can't get a route for {8,8,8,8}
for instance, because I only have information on directly attached networks. If I add the default route to the mix (via 192.168.1.1), I get:
route({8,8,8,8}, routes() ++ [{{192,168,1,1}, {{0,0,0,0},{0,0,0,0}}}]).
[{{192,168,1,1},{{0,0,0,0},{0,0,0,0}}}]
route({127,0,0,1}, routes() ++ [{{192,168,1,1}, {{0,0,0,0},{0,0,0,0}}}]).
[{"lo0",{{127,0,0,1},{255,0,0,0}}},
{{192,168,1,1},{{0,0,0,0},{0,0,0,0}}}]
From here you'd have to a) come up with that extra route information I had to add manually (call os:cmd("netstat -rn") maybe?) and b) implement a second level of lookup for routes that return gateways (you need to recursively call route until you get back a route with an interface name -- a directly attached network, not a gateway address).
The above code is available as a gist: interfaces.erl
精彩评论