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]) if srv_pkey == cli_pkey do exit :normal end # 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