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)
|
stream_id(8), payload_len(4), payload(payload_len)
|
||||||
|
|
||||||
Frame types are `open = 1`, `data = 2`, `close = 3`, and `error = 4`.
|
Frame types are `open = 1`, `data = 2`, `close = 3`, and `error = 4`.
|
||||||
`open` payload is RelayKit JSON: `host`, `port`, `useTLS`, and `method`.
|
`open` payload is binary encoded as:
|
||||||
The server is a transparent TCP relay: `useTLS` is parsed for client
|
|
||||||
compatibility, but the upstream connection is always raw TCP.
|
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).
|
-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]).
|
-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()},
|
peer :: {inet:ip_address(), inet:port_number()},
|
||||||
idle_timeout :: timeout(),
|
idle_timeout :: timeout(),
|
||||||
connect_timeout :: timeout(),
|
connect_timeout :: timeout(),
|
||||||
|
users = #{} :: #{binary() => binary()},
|
||||||
streams = #{} :: #{non_neg_integer() => stream()},
|
streams = #{} :: #{non_neg_integer() => stream()},
|
||||||
sockets = #{} :: #{inet:socket() => non_neg_integer()}
|
sockets = #{} :: #{inet:socket() => non_neg_integer()}
|
||||||
}).
|
}).
|
||||||
|
|
||||||
start_link(Transport, Peer, IdleTimeout, ConnectTimeout) ->
|
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)]),
|
logger:debug("UDP peer connected: ~s", [esockd:format(Peer)]),
|
||||||
{ok, #state{
|
{ok, #state{
|
||||||
server = Server,
|
server = Server,
|
||||||
peer = Peer,
|
peer = Peer,
|
||||||
idle_timeout = IdleTimeout,
|
idle_timeout = IdleTimeout,
|
||||||
connect_timeout = ConnectTimeout
|
connect_timeout = ConnectTimeout,
|
||||||
|
users = normalize_users(Users)
|
||||||
}, IdleTimeout}.
|
}, IdleTimeout}.
|
||||||
|
|
||||||
handle_call(_Request, _From, State = #state{idle_timeout = 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};
|
{stop, normal, State};
|
||||||
handle_info({datagram, Server, Packet}, State = #state{server = Server}) ->
|
handle_info({datagram, Server, Packet}, State = #state{server = Server}) ->
|
||||||
handle_datagram(Packet, State);
|
handle_datagram(Packet, State);
|
||||||
|
|
||||||
handle_info({tcp, Socket, Data}, State) ->
|
handle_info({tcp, Socket, Data}, State) ->
|
||||||
handle_remote_data(Socket, Data, State);
|
handle_remote_data(Socket, Data, State);
|
||||||
handle_info({tcp_closed, Socket}, State) ->
|
handle_info({tcp_closed, Socket}, State) ->
|
||||||
handle_remote_closed(Socket, State);
|
handle_remote_closed(Socket, State);
|
||||||
handle_info({tcp_error, Socket, Reason}, State) ->
|
handle_info({tcp_error, Socket, Reason}, State) ->
|
||||||
handle_remote_error(Socket, Reason, State);
|
handle_remote_error(Socket, Reason, State);
|
||||||
|
|
||||||
handle_info(timeout, State = #state{peer = Peer}) ->
|
handle_info(timeout, State = #state{peer = Peer}) ->
|
||||||
logger:debug("UDP peer idle timeout: ~s", [esockd:format(Peer)]),
|
logger:debug("UDP peer idle timeout: ~s", [esockd:format(Peer)]),
|
||||||
{stop, normal, State};
|
{stop, normal, State};
|
||||||
@ -97,7 +104,15 @@ handle_open(StreamId, Payload, State = #state{streams = Streams}) ->
|
|||||||
false ->
|
false ->
|
||||||
case relay_server_udp_protocol:decode_open_request(Payload) of
|
case relay_server_udp_protocol:decode_open_request(Payload) of
|
||||||
{ok, Request} ->
|
{ok, Request} ->
|
||||||
|
case authenticate(Request, State#state.users) of
|
||||||
|
ok ->
|
||||||
open_stream(StreamId, Request, State);
|
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} ->
|
{error, Reason} ->
|
||||||
send_error(StreamId, format_error(Reason), State),
|
send_error(StreamId, format_error(Reason), State),
|
||||||
State
|
State
|
||||||
@ -225,3 +240,54 @@ format_error(Reason) when is_binary(Reason) ->
|
|||||||
Reason;
|
Reason;
|
||||||
format_error(Reason) ->
|
format_error(Reason) ->
|
||||||
iolist_to_binary(io_lib:format("~p", [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),
|
IdleTimeout = proplists:get_value(idle_timeout, Props, ?DEFAULT_IDLE_TIMEOUT),
|
||||||
ConnectTimeout = proplists:get_value(connect_timeout, Props, ?DEFAULT_CONNECT_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),
|
UdpOptions = proplists:get_value(udp_options, Props, ?DEFAULT_UDP_OPTIONS),
|
||||||
MaxConnections = proplists:get_value(max_connections, Props, ?DEFAULT_MAX_CONNECTIONS),
|
MaxConnections = proplists:get_value(max_connections, Props, ?DEFAULT_MAX_CONNECTIONS),
|
||||||
AccessRules = proplists:get_value(access_rules, Props, [{allow, all}]),
|
AccessRules = proplists:get_value(access_rules, Props, [{allow, all}]),
|
||||||
@ -54,7 +55,7 @@ init([]) ->
|
|||||||
],
|
],
|
||||||
Opts = maybe_add(max_conn_rate, Props, BaseOpts),
|
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
|
case esockd:open_udp(Listener, ListenOn, Opts, MFA) of
|
||||||
{ok, Pid} ->
|
{ok, Pid} ->
|
||||||
logger:info("UDP listener ~p started on ~s",
|
logger:info("UDP listener ~p started on ~s",
|
||||||
|
|||||||
@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
-module(relay_server_udp_protocol).
|
-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(MAGIC, <<"RKP1">>).
|
||||||
-define(VERSION, 1).
|
-define(VERSION, 1).
|
||||||
@ -31,34 +32,39 @@ encode(Type, StreamId, Payload) when is_binary(Payload) ->
|
|||||||
PayloadLen = byte_size(Payload),
|
PayloadLen = byte_size(Payload),
|
||||||
<<"RKP1", ?VERSION:8, TypeNo:8, 0:16, StreamId:64, PayloadLen:32, Payload/binary>>.
|
<<"RKP1", ?VERSION:8, TypeNo:8, 0:16, StreamId:64, PayloadLen:32, Payload/binary>>.
|
||||||
|
|
||||||
decode_open_request(Payload) ->
|
decode_open_request(<<HostLen:16, Host:HostLen/binary, Port:16,
|
||||||
try json:decode(Payload) of
|
UsernameLen:16, Username:UsernameLen/binary,
|
||||||
#{
|
PasswordLen:16, Password:PasswordLen/binary>>)
|
||||||
<<"host">> := Host,
|
when HostLen > 0, Port > 0 ->
|
||||||
<<"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, #{
|
{ok, #{
|
||||||
host => Host,
|
host => Host,
|
||||||
port => Port,
|
port => Port,
|
||||||
use_tls => UseTLS,
|
username => Username,
|
||||||
method => Method
|
password => Password
|
||||||
}};
|
}};
|
||||||
_Other ->
|
decode_open_request(_Payload) ->
|
||||||
{error, invalid_open_request}
|
{error, invalid_open_request}.
|
||||||
catch
|
|
||||||
_Class:_Reason ->
|
encode_open_request(Host, Port) ->
|
||||||
{error, invalid_open_request}
|
encode_open_request(Host, Port, <<>>, <<>>).
|
||||||
end.
|
|
||||||
|
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(1) -> open;
|
||||||
type(2) -> data;
|
type(2) -> data;
|
||||||
|
|||||||
@ -8,6 +8,10 @@
|
|||||||
{connect_timeout, 5000},
|
{connect_timeout, 5000},
|
||||||
{max_connections, 1024},
|
{max_connections, 1024},
|
||||||
{udp_options, [binary, {reuseaddr, true}]}
|
{udp_options, [binary, {reuseaddr, true}]}
|
||||||
|
]},
|
||||||
|
|
||||||
|
{users, [
|
||||||
|
{admin, "v7@Qm!2z#R8$pL4^xT?K"}
|
||||||
]}
|
]}
|
||||||
]},
|
]},
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user