diff options
Diffstat (limited to 'shard/lib/net')
-rw-r--r-- | shard/lib/net/tcpconn.ex | 106 | ||||
-rw-r--r-- | shard/lib/net/tcpserver.ex | 26 |
2 files changed, 132 insertions, 0 deletions
diff --git a/shard/lib/net/tcpconn.ex b/shard/lib/net/tcpconn.ex new file mode 100644 index 0000000..44669bb --- /dev/null +++ b/shard/lib/net/tcpconn.ex @@ -0,0 +1,106 @@ +defmodule SNet.TCPConn do + use GenServer, restart: :temporary + require Salty.Box.Curve25519xchacha20poly1305, as: Box + require Salty.Sign.Ed25519, as: Sign + require Logger + + def start_link(state) do + GenServer.start_link(__MODULE__, state) + end + + def init(state) do + GenServer.cast(self(), :handshake) + {:ok, state} + end + + def handle_call(:get_host_str, _from, state) do + {:reply, "#{state.his_pkey|>Base.encode16|>String.downcase}@#{to_string(:inet_parse.ntoa(state.addr))}:#{state.port}", state} + end + + def handle_cast(:handshake, state) do + socket = state.socket + + {srv_pkey, srv_skey} = Shard.Identity.get_keypair + {:ok, sess_pkey, sess_skey} = Box.keypair + {:ok, challenge} = Salty.Random.buf 32 + + # Exchange public keys and challenge + hello = {srv_pkey, sess_pkey, challenge, state.my_port} + :gen_tcp.send(socket, :erlang.term_to_binary hello) + {:ok, pkt} = :gen_tcp.recv(socket, 0) + {cli_pkey, cli_sess_pkey, cli_challenge, his_port} = :erlang.binary_to_term(pkt, [:safe]) + + # Do challenge and check their challenge + {:ok, cli_challenge_sign} = Sign.sign_detached(cli_challenge, srv_skey) + pkt = encode_pkt(cli_challenge_sign, cli_sess_pkey, sess_skey) + :gen_tcp.send(socket, pkt) + + {:ok, pkt} = :gen_tcp.recv(socket, 0) + challenge_sign = decode_pkt(pkt, cli_sess_pkey, sess_skey) + :ok = Sign.verify_detached(challenge_sign, challenge, cli_pkey) + + expected_suffix = Application.get_env(:shard, :peer_id_suffix) + len = byte_size(expected_suffix) + ^len = :binary.longest_common_suffix([cli_pkey, expected_suffix]) + + # Connected + :inet.setopts(socket, [active: true]) + + {:ok, {addr, port}} = :inet.peername socket + state =%{ socket: socket, + my_pkey: srv_pkey, + my_skey: srv_skey, + his_pkey: cli_pkey, + conn_my_pkey: sess_pkey, + conn_my_skey: sess_skey, + conn_his_pkey: cli_sess_pkey, + addr: addr, + port: port, + his_port: his_port + } + GenServer.cast(Shard.Manager, {:peer_up, cli_pkey, self(), addr, his_port}) + Logger.info "New peer: #{print_id state} at #{inspect addr}:#{port}" + + {:noreply, state} + end + + def handle_cast({:send_msg, msg}, state) do + msgbin = :erlang.term_to_binary msg + enc = encode_pkt(msgbin, state.conn_his_pkey, state.conn_my_skey) + :gen_tcp.send(state.socket, enc) + {:noreply, state} + end + + defp encode_pkt(pkt, pk, sk) do + {:ok, n} = Salty.Random.buf Box.noncebytes + {:ok, msg} = Box.easy(pkt, n, pk, sk) + n <> msg + end + + defp decode_pkt(pkt, pk, sk) do + n = binary_part(pkt, 0, Box.noncebytes) + enc = binary_part(pkt, Box.noncebytes, (byte_size pkt) - Box.noncebytes) + {:ok, msg} = Box.open_easy(enc, n, pk, sk) + msg + end + + def handle_info({:tcp, _socket, raw_data}, state) do + msg = decode_pkt(raw_data, state.conn_his_pkey, state.conn_my_skey) + msg_data = :erlang.binary_to_term(msg, [:safe]) + Shard.Manager.dispatch(state.his_pkey, msg_data) + {:noreply, state} + end + + def handle_info({:tcp_closed, _socket}, state) do + Logger.info "Disconnected: #{print_id state} at #{inspect state.addr}:#{state.port}" + GenServer.cast(Shard.Manager, {:peer_down, state.his_pkey, state.addr, state.his_port}) + exit(:normal) + end + + defp print_id(state) do + state.his_pkey + |> binary_part(0, 8) + |> Base.encode16 + |> String.downcase + end +end diff --git a/shard/lib/net/tcpserver.ex b/shard/lib/net/tcpserver.ex new file mode 100644 index 0000000..46552a4 --- /dev/null +++ b/shard/lib/net/tcpserver.ex @@ -0,0 +1,26 @@ +defmodule SNet.TCPServer do + require Logger + use Task, restart: :permanent + + def start_link(port) do + Task.start_link(__MODULE__, :accept, [port]) + end + + @doc """ + Starts accepting connections on the given `port`. + """ + def accept(port) do + {:ok, socket} = :gen_tcp.listen(port, + [:binary, packet: 2, active: false, reuseaddr: true]) + Logger.info "Accepting connections on port #{port}" + loop_acceptor(socket, port) + end + + defp loop_acceptor(socket, my_port) do + {:ok, client} = :gen_tcp.accept(socket) + {:ok, pid} = DynamicSupervisor.start_child(Shard.DynamicSupervisor, {SNet.TCPConn, %{socket: client, my_port: my_port}}) + :ok = :gen_tcp.controlling_process(client, pid) + loop_acceptor(socket, my_port) + end +end + |