-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This commit tries to improve two issues 1) A free port is obtained by setting the port value as zero and the OS will bind to a free port. We immediately close the port and then later create another socket on the same port. The issue with the approach is, OS could allocate the same port to another because we have closed the port. This leads to a situation where more than one bypass server could listen on the same port (this is possible because of SO_REUSEPORT flag). The issue is fixed by not closing the socket. 2) Bypass exposes a down api, which closes the socket. The issue here is the same as above, the OS is free to allocate the port to others. The current solution tries to fix the issue by keeping track of which test owns which ports and try not to reuse the same ports. This is still not foolproof, there is a small interval during which the socket is active, but better than the old logic.
- Loading branch information
1 parent
6436504
commit d0ba183
Showing
4 changed files
with
136 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
defmodule Bypass.FreePort do | ||
alias Bypass.Utils | ||
use GenServer | ||
|
||
defstruct [:ports, :owners] | ||
|
||
def start_link([]) do | ||
GenServer.start_link(__MODULE__, [], name: __MODULE__) | ||
end | ||
|
||
def reserve(owner) do | ||
GenServer.call(__MODULE__, {:reserve, owner}) | ||
end | ||
|
||
def init([]) do | ||
{:ok, %__MODULE__{ports: MapSet.new(), owners: %{}}} | ||
end | ||
|
||
def handle_call({:reserve, owner}, _from, state) do | ||
ref = Process.monitor(owner) | ||
{state, reply} = find_free_port(state, owner, ref, 0) | ||
{:reply, reply, state} | ||
end | ||
|
||
def handle_info({:DOWN, ref, _type, pid, _reason}, state) do | ||
state = | ||
case Map.pop(state.owners, {pid, ref}) do | ||
{nil, _} -> | ||
state | ||
|
||
{port, owners} -> | ||
%{state | ports: MapSet.delete(state.ports, port), owners: owners} | ||
end | ||
|
||
{:noreply, state} | ||
end | ||
|
||
def handle_info(_msg, state) do | ||
{:noreply, state} | ||
end | ||
|
||
defp find_free_port(state, _owner, _ref, 10 = _attempt), | ||
do: {state, {:error, :too_many_attempts}} | ||
|
||
defp find_free_port(state, owner, ref, attempt) do | ||
case :ranch_tcp.listen(Utils.so_reuseport() ++ [ip: Utils.listen_ip(), port: 0]) do | ||
{:ok, socket} -> | ||
{:ok, port} = :inet.port(socket) | ||
|
||
if MapSet.member?(state.ports, port) do | ||
true = :erlang.port_close(socket) | ||
|
||
find_free_port(state, owner, ref, attempt + 1) | ||
else | ||
state = %{ | ||
state | ||
| ports: MapSet.put(state.ports, port), | ||
owners: Map.put_new(state.owners, {owner, ref}, port) | ||
} | ||
|
||
{state, {:ok, socket}} | ||
end | ||
|
||
{:error, reason} -> | ||
{state, {:error, reason}} | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters