fix
This commit is contained in:
parent
d6fb2a480f
commit
9d5bc114ed
20
README.md
20
README.md
@ -25,6 +25,20 @@ Each UDP datagram is one `RKP1` frame:
|
||||
stream_id(8), payload_len(4), payload(payload_len)
|
||||
|
||||
Frame types are `open = 1`, `data = 2`, `close = 3`, and `error = 4`.
|
||||
`open` payload is RelayKit JSON: `host`, `port`, `useTLS`, and `method`.
|
||||
The server is a transparent TCP relay: `useTLS` is parsed for client
|
||||
compatibility, but the upstream connection is always raw TCP.
|
||||
`open` payload is binary encoded as:
|
||||
|
||||
host_len(2), host(host_len), port(2),
|
||||
username_len(2), username(username_len),
|
||||
password_len(2), password(password_len)
|
||||
|
||||
Integer fields are unsigned big-endian values. `host`, `username`, and
|
||||
`password` are binary strings. The server is a transparent TCP relay, so TLS
|
||||
negotiation, HTTP methods, and request bytes are carried inside subsequent
|
||||
`data` frames without being interpreted by the server.
|
||||
|
||||
`username` and `password` are checked against the `relay_server` application
|
||||
`users` config:
|
||||
|
||||
{users, [
|
||||
{admin, "password"}
|
||||
]}
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
-behaviour(gen_server).
|
||||
|
||||
-export([start_link/4]).
|
||||
-export([start_link/4, start_link/5]).
|
||||
|
||||
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
||||
|
||||
@ -27,20 +27,25 @@
|
||||
peer :: {inet:ip_address(), inet:port_number()},
|
||||
idle_timeout :: timeout(),
|
||||
connect_timeout :: timeout(),
|
||||
users = #{} :: #{binary() => binary()},
|
||||
streams = #{} :: #{non_neg_integer() => stream()},
|
||||
sockets = #{} :: #{inet:socket() => non_neg_integer()}
|
||||
}).
|
||||
|
||||
start_link(Transport, Peer, IdleTimeout, ConnectTimeout) ->
|
||||
gen_server:start_link(?MODULE, [Transport, Peer, IdleTimeout, ConnectTimeout], []).
|
||||
start_link(Transport, Peer, IdleTimeout, ConnectTimeout, load_users()).
|
||||
|
||||
init([{udp, Server, _Sock}, Peer, IdleTimeout, ConnectTimeout]) ->
|
||||
start_link(Transport, Peer, IdleTimeout, ConnectTimeout, Users) ->
|
||||
gen_server:start_link(?MODULE, [Transport, Peer, IdleTimeout, ConnectTimeout, Users], []).
|
||||
|
||||
init([{udp, Server, _Sock}, Peer, IdleTimeout, ConnectTimeout, Users]) ->
|
||||
logger:debug("UDP peer connected: ~s", [esockd:format(Peer)]),
|
||||
{ok, #state{
|
||||
server = Server,
|
||||
peer = Peer,
|
||||
idle_timeout = IdleTimeout,
|
||||
connect_timeout = ConnectTimeout
|
||||
connect_timeout = ConnectTimeout,
|
||||
users = normalize_users(Users)
|
||||
}, IdleTimeout}.
|
||||
|
||||
handle_call(_Request, _From, State = #state{idle_timeout = IdleTimeout}) ->
|
||||
@ -54,12 +59,14 @@ handle_info({datagram, Server, <<"stop">>}, State = #state{server = Server, peer
|
||||
{stop, normal, State};
|
||||
handle_info({datagram, Server, Packet}, State = #state{server = Server}) ->
|
||||
handle_datagram(Packet, State);
|
||||
|
||||
handle_info({tcp, Socket, Data}, State) ->
|
||||
handle_remote_data(Socket, Data, State);
|
||||
handle_info({tcp_closed, Socket}, State) ->
|
||||
handle_remote_closed(Socket, State);
|
||||
handle_info({tcp_error, Socket, Reason}, State) ->
|
||||
handle_remote_error(Socket, Reason, State);
|
||||
|
||||
handle_info(timeout, State = #state{peer = Peer}) ->
|
||||
logger:debug("UDP peer idle timeout: ~s", [esockd:format(Peer)]),
|
||||
{stop, normal, State};
|
||||
@ -97,7 +104,15 @@ handle_open(StreamId, Payload, State = #state{streams = Streams}) ->
|
||||
false ->
|
||||
case relay_server_udp_protocol:decode_open_request(Payload) of
|
||||
{ok, Request} ->
|
||||
open_stream(StreamId, Request, State);
|
||||
case authenticate(Request, State#state.users) of
|
||||
ok ->
|
||||
open_stream(StreamId, Request, State);
|
||||
error ->
|
||||
logger:warning("UDP relay stream ~p authentication failed from ~s",
|
||||
[StreamId, esockd:format(State#state.peer)]),
|
||||
send_error(StreamId, <<"authentication failed">>, State),
|
||||
State
|
||||
end;
|
||||
{error, Reason} ->
|
||||
send_error(StreamId, format_error(Reason), State),
|
||||
State
|
||||
@ -225,3 +240,54 @@ format_error(Reason) when is_binary(Reason) ->
|
||||
Reason;
|
||||
format_error(Reason) ->
|
||||
iolist_to_binary(io_lib:format("~p", [Reason])).
|
||||
|
||||
load_users() ->
|
||||
application:get_env(relay_server, users, []).
|
||||
|
||||
normalize_users(Users) when is_list(Users) ->
|
||||
lists:foldl(fun normalize_user/2, #{}, Users);
|
||||
normalize_users(_Users) ->
|
||||
#{}.
|
||||
|
||||
normalize_user({Username0, Password0}, Users) ->
|
||||
case {to_credential_binary(Username0), to_credential_binary(Password0)} of
|
||||
{{ok, Username}, {ok, Password}} when byte_size(Username) > 0 ->
|
||||
maps:put(Username, Password, Users);
|
||||
_ ->
|
||||
Users
|
||||
end;
|
||||
normalize_user(_User, Users) ->
|
||||
Users.
|
||||
|
||||
to_credential_binary(Value) when is_binary(Value) ->
|
||||
{ok, Value};
|
||||
to_credential_binary(Value) when is_atom(Value) ->
|
||||
{ok, atom_to_binary(Value, utf8)};
|
||||
to_credential_binary(Value) when is_list(Value) ->
|
||||
try unicode:characters_to_binary(Value) of
|
||||
Binary when is_binary(Binary) -> {ok, Binary};
|
||||
_Other -> error
|
||||
catch
|
||||
_Class:_Reason -> error
|
||||
end;
|
||||
to_credential_binary(_Value) ->
|
||||
error.
|
||||
|
||||
authenticate(#{username := Username, password := Password}, Users) ->
|
||||
case maps:get(Username, Users, undefined) of
|
||||
undefined ->
|
||||
error;
|
||||
ExpectedPassword ->
|
||||
case secure_equal(Password, ExpectedPassword) of
|
||||
true -> ok;
|
||||
false -> error
|
||||
end
|
||||
end.
|
||||
|
||||
secure_equal(Left, Right) when is_binary(Left), is_binary(Right) ->
|
||||
(byte_size(Left) =:= byte_size(Right)) andalso secure_equal(Left, Right, 0) =:= 0.
|
||||
|
||||
secure_equal(<<L:8, LRest/binary>>, <<R:8, RRest/binary>>, Diff) ->
|
||||
secure_equal(LRest, RRest, Diff bor (L bxor R));
|
||||
secure_equal(<<>>, <<>>, Diff) ->
|
||||
Diff.
|
||||
|
||||
@ -43,6 +43,7 @@ init([]) ->
|
||||
|
||||
IdleTimeout = proplists:get_value(idle_timeout, Props, ?DEFAULT_IDLE_TIMEOUT),
|
||||
ConnectTimeout = proplists:get_value(connect_timeout, Props, ?DEFAULT_CONNECT_TIMEOUT),
|
||||
Users = application:get_env(relay_server, users, []),
|
||||
UdpOptions = proplists:get_value(udp_options, Props, ?DEFAULT_UDP_OPTIONS),
|
||||
MaxConnections = proplists:get_value(max_connections, Props, ?DEFAULT_MAX_CONNECTIONS),
|
||||
AccessRules = proplists:get_value(access_rules, Props, [{allow, all}]),
|
||||
@ -54,7 +55,7 @@ init([]) ->
|
||||
],
|
||||
Opts = maybe_add(max_conn_rate, Props, BaseOpts),
|
||||
|
||||
MFA = {relay_server_udp_handler, start_link, [IdleTimeout, ConnectTimeout]},
|
||||
MFA = {relay_server_udp_handler, start_link, [IdleTimeout, ConnectTimeout, Users]},
|
||||
case esockd:open_udp(Listener, ListenOn, Opts, MFA) of
|
||||
{ok, Pid} ->
|
||||
logger:info("UDP listener ~p started on ~s",
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
|
||||
-module(relay_server_udp_protocol).
|
||||
|
||||
-export([decode/1, encode/3, decode_open_request/1]).
|
||||
-export([decode/1, encode/3, decode_open_request/1, encode_open_request/2,
|
||||
encode_open_request/4]).
|
||||
|
||||
-define(MAGIC, <<"RKP1">>).
|
||||
-define(VERSION, 1).
|
||||
@ -31,34 +32,39 @@ encode(Type, StreamId, Payload) when is_binary(Payload) ->
|
||||
PayloadLen = byte_size(Payload),
|
||||
<<"RKP1", ?VERSION:8, TypeNo:8, 0:16, StreamId:64, PayloadLen:32, Payload/binary>>.
|
||||
|
||||
decode_open_request(Payload) ->
|
||||
try json:decode(Payload) of
|
||||
#{
|
||||
<<"host">> := Host,
|
||||
<<"port">> := Port,
|
||||
<<"useTLS">> := UseTLS,
|
||||
<<"method">> := Method
|
||||
} when
|
||||
is_binary(Host),
|
||||
byte_size(Host) > 0,
|
||||
is_integer(Port),
|
||||
Port > 0,
|
||||
Port =< 65535,
|
||||
is_boolean(UseTLS),
|
||||
is_binary(Method)
|
||||
->
|
||||
{ok, #{
|
||||
host => Host,
|
||||
port => Port,
|
||||
use_tls => UseTLS,
|
||||
method => Method
|
||||
}};
|
||||
_Other ->
|
||||
{error, invalid_open_request}
|
||||
catch
|
||||
_Class:_Reason ->
|
||||
{error, invalid_open_request}
|
||||
end.
|
||||
decode_open_request(<<HostLen:16, Host:HostLen/binary, Port:16,
|
||||
UsernameLen:16, Username:UsernameLen/binary,
|
||||
PasswordLen:16, Password:PasswordLen/binary>>)
|
||||
when HostLen > 0, Port > 0 ->
|
||||
{ok, #{
|
||||
host => Host,
|
||||
port => Port,
|
||||
username => Username,
|
||||
password => Password
|
||||
}};
|
||||
decode_open_request(_Payload) ->
|
||||
{error, invalid_open_request}.
|
||||
|
||||
encode_open_request(Host, Port) ->
|
||||
encode_open_request(Host, Port, <<>>, <<>>).
|
||||
|
||||
encode_open_request(Host, Port, Username, Password) when
|
||||
is_binary(Host),
|
||||
byte_size(Host) > 0,
|
||||
byte_size(Host) =< 65535,
|
||||
is_integer(Port),
|
||||
Port > 0,
|
||||
Port =< 65535,
|
||||
is_binary(Username),
|
||||
byte_size(Username) =< 65535,
|
||||
is_binary(Password),
|
||||
byte_size(Password) =< 65535 ->
|
||||
HostLen = byte_size(Host),
|
||||
UsernameLen = byte_size(Username),
|
||||
PasswordLen = byte_size(Password),
|
||||
<<HostLen:16, Host/binary, Port:16,
|
||||
UsernameLen:16, Username/binary,
|
||||
PasswordLen:16, Password/binary>>.
|
||||
|
||||
type(1) -> open;
|
||||
type(2) -> data;
|
||||
|
||||
@ -8,6 +8,10 @@
|
||||
{connect_timeout, 5000},
|
||||
{max_connections, 1024},
|
||||
{udp_options, [binary, {reuseaddr, true}]}
|
||||
]},
|
||||
|
||||
{users, [
|
||||
{admin, "v7@Qm!2z#R8$pL4^xT?K"}
|
||||
]}
|
||||
]},
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user