├── .gitignore ├── .travis.yml ├── README.md ├── config ├── .credo.exs └── config.exs ├── dialyzer.ignore-warnings ├── lib ├── quic.ex └── quic │ ├── connection.ex │ ├── frame │ └── frame_types.ex │ ├── header │ ├── header_long.ex │ └── header_short.ex │ ├── packet │ ├── packet.ex │ └── version_negotiation.ex │ └── util │ └── bitstring_helpers.ex ├── mix.exs ├── mix.lock └── test ├── quic ├── connection_test.exs ├── frame │ └── frame_types_test.exs └── packet │ └── version_negotiation_test.exs ├── quic_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/vim,elixir,intellij 2 | 3 | ### Elixir ### 4 | /_build 5 | /cover 6 | /deps 7 | /doc 8 | /.fetch 9 | erl_crash.dump 10 | *.ez 11 | *.beam 12 | 13 | ### Elixir Patch ### 14 | ### Intellij ### 15 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 16 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 17 | 18 | # User-specific stuff: 19 | .idea/**/workspace.xml 20 | .idea/**/tasks.xml 21 | .idea/dictionaries 22 | 23 | # Sensitive or high-churn files: 24 | .idea/**/dataSources/ 25 | .idea/**/dataSources.ids 26 | .idea/**/dataSources.xml 27 | .idea/**/dataSources.local.xml 28 | .idea/**/sqlDataSources.xml 29 | .idea/**/dynamic.xml 30 | .idea/**/uiDesigner.xml 31 | 32 | # Gradle: 33 | .idea/**/gradle.xml 34 | .idea/**/libraries 35 | 36 | # CMake 37 | cmake-build-debug/ 38 | 39 | # Mongo Explorer plugin: 40 | .idea/**/mongoSettings.xml 41 | 42 | ## File-based project format: 43 | *.iws 44 | 45 | ## Plugin-specific files: 46 | 47 | # IntelliJ 48 | /out/ 49 | 50 | # mpeltonen/sbt-idea plugin 51 | .idea_modules/ 52 | 53 | # JIRA plugin 54 | atlassian-ide-plugin.xml 55 | 56 | # Cursive Clojure plugin 57 | .idea/replstate.xml 58 | 59 | # Ruby plugin and RubyMine 60 | /.rakeTasks 61 | 62 | # Crashlytics plugin (for Android Studio and IntelliJ) 63 | com_crashlytics_export_strings.xml 64 | crashlytics.properties 65 | crashlytics-build.properties 66 | fabric.properties 67 | 68 | ### Intellij Patch ### 69 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 70 | 71 | # *.iml 72 | # modules.xml 73 | # .idea/misc.xml 74 | # *.ipr 75 | 76 | # Sonarlint plugin 77 | .idea/sonarlint 78 | 79 | ### Vim ### 80 | # swap 81 | [._]*.s[a-v][a-z] 82 | [._]*.sw[a-p] 83 | [._]s[a-v][a-z] 84 | [._]sw[a-p] 85 | # session 86 | Session.vim 87 | # temporary 88 | .netrwhist 89 | *~ 90 | # auto-generated tag files 91 | tags 92 | 93 | # End of https://www.gitignore.io/api/vim,elixir,intellij 94 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | 3 | elixir: 4 | - 1.5.2 5 | 6 | otp_release: 7 | - 20.1 8 | 9 | script: 10 | - mix dialyzer 11 | - mix credo --strict 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # quic-elixir 2 | 3 | [![Build Status](https://travis-ci.org/Viddi/quic-elixir.svg?branch=master)](https://travis-ci.org/Viddi/quic-elixir) 4 | 5 | **TODO: Add description** 6 | -------------------------------------------------------------------------------- /config/.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | files: %{ 6 | included: ["lib/"], 7 | excluded: [~r"/_build/", ~r"/deps/"] 8 | }, 9 | requires: [], 10 | check_for_updates: true, 11 | strict: false, 12 | checks: [ 13 | {Credo.Check.Readability.MaxLineLength, max_length: 110}, 14 | {Credo.Check.Refactor.PipeChainStart, false}, 15 | {Credo.Check.Design.TagTODO, false}, 16 | {Credo.Check.Design.AliasUsage, false}, 17 | {Credo.Check.Readability.ParenthesesOnZeroArityDefs, false} 18 | ] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure your application as: 12 | # 13 | # config :quic, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:quic, :key) 18 | # 19 | # You can also configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /dialyzer.ignore-warnings: -------------------------------------------------------------------------------- 1 | :0: Unknown function 'Elixir.QUIC.Type.Atom':'__impl__'/1 2 | :0: Unknown function 'Elixir.QUIC.Type.BitString':'__impl__'/1 3 | :0: Unknown function 'Elixir.QUIC.Type.Float':'__impl__'/1 4 | :0: Unknown function 'Elixir.QUIC.Type.Function':'__impl__'/1 5 | :0: Unknown function 'Elixir.QUIC.Type.Integer':'__impl__'/1 6 | :0: Unknown function 'Elixir.QUIC.Type.List':'__impl__'/1 7 | :0: Unknown function 'Elixir.QUIC.Type.Map':'__impl__'/1 8 | :0: Unknown function 'Elixir.QUIC.Type.PID':'__impl__'/1 9 | :0: Unknown function 'Elixir.QUIC.Type.Port':'__impl__'/1 10 | :0: Unknown function 'Elixir.QUIC.Type.Reference':'__impl__'/1 11 | :0: Unknown function 'Elixir.QUIC.Type.Tuple':'__impl__'/1 12 | -------------------------------------------------------------------------------- /lib/quic.ex: -------------------------------------------------------------------------------- 1 | defmodule QUIC do 2 | @moduledoc """ 3 | Documentation for QUIC. 4 | """ 5 | 6 | @doc """ 7 | A list of all supported QUIC versions. 8 | 9 | ## Examples 10 | 11 | iex> QUIC.supported_versions() 12 | [40] 13 | """ 14 | @spec supported_versions() :: [40, ...] 15 | def supported_versions(), do: [40] 16 | 17 | ## Socket API 18 | 19 | @spec start() :: GenServer.on_start 20 | @spec start(GenServer.options) :: GenServer.on_start 21 | def start(opts \\ []) do 22 | QUIC.Connection.start(opts) 23 | end 24 | 25 | @spec start_link() :: GenServer.on_start 26 | @spec start_link(GenServer.options) :: GenServer.on_start 27 | def start_link(opts \\ []) do 28 | QUIC.Connection.start_link(opts) 29 | end 30 | 31 | @spec open(pid, integer, [any]) :: term 32 | def open(pid, port, opts) do 33 | QUIC.Connection.open(pid, port, opts) 34 | end 35 | 36 | @spec close(pid) :: tuple 37 | def close(pid) do 38 | QUIC.Connection.close(pid) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/quic/connection.ex: -------------------------------------------------------------------------------- 1 | defmodule QUIC.Connection do 2 | @moduledoc """ 3 | A GenServer process for a single QUIC connection. 4 | """ 5 | 6 | use GenServer 7 | 8 | require Logger 9 | 10 | @gen_udp_opts [ 11 | :binary, 12 | active: 10, 13 | add_membership: {{127, 0, 0, 1}, {0, 0, 0, 0}}, 14 | multicast_if: {0, 0, 0, 0} 15 | ] 16 | 17 | @typedoc """ 18 | The data that needs to be stored during 19 | the lifecycle of a QUIC connection. 20 | 21 | socket: reference to the gen_udp socket. 22 | """ 23 | @type t :: %__MODULE__{ 24 | socket: port 25 | } 26 | 27 | defstruct [:socket] 28 | 29 | @doc false 30 | def init(_args) do 31 | {:ok, %__MODULE__{}} 32 | end 33 | 34 | @doc """ 35 | Overrides GenServer start/3 function. 36 | 37 | ## Parameters 38 | 39 | - opts: Options list passed to GenServer start/3 function. 40 | """ 41 | @spec start() :: GenServer.on_start 42 | @spec start(GenServer.options) :: GenServer.on_start 43 | def start(opts \\ []) do 44 | GenServer.start(__MODULE__, [], opts) 45 | end 46 | 47 | @doc """ 48 | Overrides GenServer start_link/3 function. 49 | 50 | ## Parameters 51 | 52 | - opts: Options list passed to GenServer start_link/3 function. 53 | """ 54 | @spec start_link() :: GenServer.on_start 55 | @spec start_link(GenServer.options) :: GenServer.on_start 56 | def start_link(opts \\ []) do 57 | GenServer.start_link(__MODULE__, [], opts) 58 | end 59 | 60 | @doc """ 61 | Opens the UDP socket on the given port. 62 | 63 | ## Parameters 64 | 65 | - pid: The process identifier for the connection. 66 | - port: The port which the socket will be created on. 67 | - opts: A list of options that will be passed to the udp socket. 68 | """ 69 | @spec open(pid, integer) :: term 70 | @spec open(pid, integer, [any]) :: term 71 | def open(pid, port, opts \\ []) do 72 | GenServer.call(pid, {:open, port, opts}) 73 | end 74 | 75 | @doc """ 76 | Only being used for testing now. 77 | 78 | ## Parameters 79 | 80 | - pid: The process identifier for the connection. 81 | - address: The recipient ip address. 82 | - port: The port of the socket. 83 | - payload: The payload to send. 84 | """ 85 | @spec send(pid, {integer, integer, integer, integer}, integer, term) :: :ok 86 | def send(pid, address, port, payload) do 87 | GenServer.cast(pid, {:send, address, port, payload}) 88 | end 89 | 90 | @doc """ 91 | Closes the QUIC connection for the given pid. 92 | 93 | ## Parameters 94 | 95 | - pid: The pid for the QUIC.Connection process. 96 | """ 97 | @spec close(pid) :: tuple 98 | def close(pid) when is_pid(pid) do 99 | Logger.info("Closing connection with pid: #{inspect(pid)}") 100 | GenServer.call(pid, :close) 101 | end 102 | 103 | ## GenServer callbacks 104 | 105 | @doc false 106 | def handle_call({:open, port, opts}, _from, state) do 107 | case open_socket(port, opts) do 108 | {:ok, socket} -> 109 | {:reply, {:ok, socket}, %{state | socket: socket}} 110 | {:error, reason} -> 111 | {:reply, {:error, reason}, state} 112 | end 113 | end 114 | 115 | @doc false 116 | def handle_call(:close, _from, state) do 117 | {:stop, :normal, :ok, state} 118 | end 119 | 120 | @doc false 121 | def handle_cast({:send, address, port, packet}, _from, state) do 122 | if state.socket do 123 | :gen_udp.send(state.socket, address, port, packet) 124 | {:noreply, state} 125 | else 126 | {:noreply, state} 127 | end 128 | end 129 | 130 | @doc false 131 | def handle_info({:udp, _socket, ip, port, packet}, state) do 132 | Logger.info("Received udp packet: #{inspect(packet)} from #{inspect(ip)}:#{inspect(port)}") 133 | {:noreply, state} 134 | end 135 | 136 | @doc false 137 | def terminate(reason, state) do 138 | Logger.info("Closing connection: #{inspect(reason)}") 139 | if state.socket do 140 | :gen_udp.close(state.socket) 141 | end 142 | 143 | :ok 144 | end 145 | 146 | ## Private functions 147 | 148 | # Opens a socket with the given options (opts[:udp_opts]), 149 | # or the default udp options provided by this module. 150 | @spec open_socket(integer, [any]) :: {:error, atom} | {:ok, port} 151 | def open_socket(port, opts) do 152 | :gen_udp.open(port, if List.first(opts) do opts else @gen_udp_opts end) 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /lib/quic/frame/frame_types.ex: -------------------------------------------------------------------------------- 1 | defmodule QUIC.Frame.Type do 2 | @moduledoc """ 3 | All QUIC Frame types. 4 | 5 | [Source](https://quicwg.github.io/base-drafts/draft-ietf-quic-transport.html#frames) 6 | """ 7 | 8 | use Bitwise 9 | 10 | @spec padding() :: 0x00 11 | def padding(), do: 0x00 12 | 13 | @spec rst_stream() :: 0x01 14 | def rst_stream(), do: 0x01 15 | 16 | @spec connection_close() :: 0x02 17 | def connection_close(), do: 0x02 18 | 19 | @spec application_close() :: 0x03 20 | def application_close(), do: 0x03 21 | 22 | @spec max_data() :: 0x04 23 | def max_data(), do: 0x04 24 | 25 | @spec max_stream_data() :: 0x05 26 | def max_stream_data(), do: 0x05 27 | 28 | @spec max_stream_id() :: 0x06 29 | def max_stream_id(), do: 0x06 30 | 31 | @spec ping() :: 0x07 32 | def ping(), do: 0x07 33 | 34 | @spec blocked() :: 0x08 35 | def blocked(), do: 0x08 36 | 37 | @spec stream_blocked() :: 0x09 38 | def stream_blocked(), do: 0x09 39 | 40 | @spec stream_id_blocked() :: 0x0a 41 | def stream_id_blocked(), do: 0x0a 42 | 43 | @spec new_connection_id() :: 0x0b 44 | def new_connection_id(), do: 0x0b 45 | 46 | @spec stop_sending() :: 0x0c 47 | def stop_sending(), do: 0x0c 48 | 49 | @spec pong() :: 0x0d 50 | def pong(), do: 0x0d 51 | 52 | @spec ack() :: 0x0e 53 | def ack(), do: 0x0e 54 | 55 | @doc """ 56 | The stream frame takes the form 0b00010XXX, i.e values 57 | ranging from 0x10 - 0x17 depending on which bits are set. 58 | 59 | Each bit represent a binary value: 60 | FIN bit: 1 61 | LEN bit: 2 62 | OFF bit: 4 63 | 64 | Based on the flags passed into this function, the base stream 65 | type value will be mutated to include each of these flags. 66 | That is, we start with 16, and then check each flag and add 67 | each flag if they are present. 68 | 69 | [Source](https://quicwg.github.io/base-drafts/draft-ietf-quic-transport.html#frame-stream) 70 | 71 | ## Parameters 72 | 73 | - fin: Whether the FIN bit is set or not. 74 | - len: Whether the LEN bit is set or not. 75 | - off: Whether the OFF bit is set or not. 76 | """ 77 | @spec stream(boolean, boolean, boolean) :: non_neg_integer 78 | def stream(fin, len, off) do 79 | 0x10 80 | |> fin_bit(fin) 81 | |> len_bit(len) 82 | |> off_bit(off) 83 | end 84 | 85 | # This is a helper function to build the stream type. If the fin bit 86 | # is set to true, then 1 will be added to the existing value 87 | # of the stream type. If the fin bit is set to false, then the 88 | # value will be left unmodified. 89 | @spec fin_bit(non_neg_integer, boolean) :: non_neg_integer 90 | defp fin_bit(n, true), do: bor(n, 1) 91 | defp fin_bit(n, false), do: n 92 | 93 | # This is a helper function to build a stream type. If the len bit 94 | # is set to true, then 2 will be added to the existing value 95 | # of the stream type. If the len bit is set to false, then the 96 | # value will be left unmodified. 97 | @spec len_bit(non_neg_integer, boolean) :: non_neg_integer 98 | defp len_bit(n, true), do: bor(n, 2) 99 | defp len_bit(n, false), do: n 100 | 101 | # This is a helper function to build a stream type. If the off bit 102 | # is set to true, then 4 will be added to the existing value 103 | # of the stream type. If the off bit is set to false, then the 104 | # value will be left unmodified. 105 | @spec off_bit(non_neg_integer, boolean) :: non_neg_integer 106 | def off_bit(n, true), do: bor(n, 4) 107 | def off_bit(n, false), do: n 108 | end 109 | -------------------------------------------------------------------------------- /lib/quic/header/header_long.ex: -------------------------------------------------------------------------------- 1 | defmodule QUIC.Header.Long do 2 | @moduledoc """ 3 | Long headers are used for packets that are sent prior to the 4 | completion of version negotiation and establishment of 1-RTT keys. 5 | Once both conditions are met, a sender SHOULD switch to sending 6 | short-form headers. While inefficient, long headers MAY be used for 7 | packets encrypted with 1-RTT keys. The long form allows for special 8 | packets, such as the Version Negotiation and Public Reset packets 9 | to be represented in this uniform fixed-length packet format. 10 | 11 | ``` 12 | 0 1 2 3 13 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 14 | +-+-+-+-+-+-+-+-+ 15 | |1| Type (7) | 16 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 17 | | | 18 | + Connection ID (64) + 19 | | | 20 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 21 | | Packet Number (32) | 22 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 23 | | Version (32) | 24 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 25 | | Payload (*) ... 26 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 27 | ``` 28 | 29 | [Source](https://quicwg.github.io/base-drafts/draft-ietf-quic-transport.html#long-header) 30 | """ 31 | 32 | @typedoc """ 33 | type: A bitmask indicating types for a packet. 34 | connection_id: The 64 bit id for the connection. 35 | packet_number: The 64 bit unsigned packet number, which is 36 | used as a part of a cryptographic nonce for packet encryption. 37 | version: The QUIC version being used for this packet. 38 | payload: The packet payload. 39 | """ 40 | @type t :: %__MODULE__{ 41 | type: integer, 42 | connection_id: integer, 43 | packet_number: integer, 44 | version: integer, 45 | payload: bitstring 46 | } 47 | 48 | @enforce_keys [:type, :connection_id, :packet_number] 49 | defstruct [:type, :connection_id, :packet_number, :version, :payload] 50 | 51 | defmodule Type do 52 | @moduledoc """ 53 | This module contains the types for all different 54 | packets that can be sent with a long header. 55 | 56 | The remaining seven bits of octet 0 contain the packet type. 57 | This field can indicate one of 128 packet types. 58 | 59 | [Source](https://quicwg.github.io/base-drafts/draft-ietf-quic-transport.html#long-packet-types) 60 | """ 61 | 62 | @doc """ 63 | 0x01 Version Negotiation packet. 64 | [Source](https://quicwg.github.io/base-drafts/draft-ietf-quic-transport.html#packet-version) 65 | """ 66 | @spec version_negotiation() :: 1 67 | def version_negotiation(), do: 1 68 | 69 | @doc """ 70 | 0x02 Client Initial packet. 71 | [Source](https://quicwg.github.io/base-drafts/draft-ietf-quic-transport.html#packet-client-initial) 72 | """ 73 | @spec client_initial() :: 2 74 | def client_initial(), do: 2 75 | 76 | @doc """ 77 | 0x03 Server Stateless Retry packet. 78 | [Source](https://quicwg.github.io/base-drafts/draft-ietf-quic-transport.html#packet-server-stateless) 79 | """ 80 | @spec server_stateless_retry() :: 3 81 | def server_stateless_retry(), do: 3 82 | 83 | @doc """ 84 | 0x04 Server Cleartext packet. 85 | [Source](https://quicwg.github.io/base-drafts/draft-ietf-quic-transport.html#packet-server-cleartext) 86 | """ 87 | @spec server_cleartext() :: 4 88 | def server_cleartext(), do: 4 89 | 90 | @doc """ 91 | 0x05 Client Cleartext packet. 92 | [Source](https://quicwg.github.io/base-drafts/draft-ietf-quic-transport.html#packet-client-cleartext) 93 | """ 94 | @spec client_cleartext() :: 5 95 | def client_cleartext(), do: 5 96 | 97 | @doc """ 98 | 0x06 0-RTT Protected packet. 99 | [Source](https://quicwg.github.io/base-drafts/draft-ietf-quic-transport.html#packet-protected) 100 | """ 101 | @spec zero_rtt_protected() :: 6 102 | def zero_rtt_protected(), do: 6 103 | end 104 | 105 | @doc """ 106 | Encodes the long header packet in to a bitstring form. 107 | 108 | ## Parameters 109 | 110 | - type: A bitmask indicating types for a packet. 111 | - connection_id: The 64 bit id for the connection. 112 | - packet_number: The 64 bit unsigned packet number, which is 113 | - used as a part of a cryptographic nonce for packet encryption. 114 | - version: The QUIC version being used for this packet. 115 | - payload: The packet payload. 116 | """ 117 | @spec encode(integer, integer, integer, integer, bitstring) :: <<_::_ * 7>> 118 | def encode(type, connection_id, packet_number, version, payload) do 119 | <<1::1, type::7, connection_id::64, packet_number::32, version::32>> <> payload 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /lib/quic/header/header_short.ex: -------------------------------------------------------------------------------- 1 | defmodule QUIC.Header.Short do 2 | @moduledoc """ 3 | The short header can be used after the version and 1-RTT keys are negotiated. 4 | 5 | ``` 6 | 0 1 2 3 7 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 8 | +-+-+-+-+-+-+-+-+ 9 | |0|C|K| Type (5)| 10 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 11 | | | 12 | + [Connection ID (64)] + 13 | | | 14 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 15 | | Packet Number (8/16/32) ... 16 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 17 | | Protected Payload (*) ... 18 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 19 | ``` 20 | 21 | [Source](https://quicwg.github.io/base-drafts/draft-ietf-quic-transport.html#short-header) 22 | """ 23 | 24 | @typedoc """ 25 | c: Connection id flag. The second bit (0x40) of the first octet 26 | indicates whether the Connection id field is present. 27 | k: Key phase. This allows a recipient of a packet to identify the 28 | packet protection keys that are used to protect the packet. 29 | type: A bitmask indicating types for a packet. 30 | connection_id: The 64 bit id for the connection. 31 | packet_number: The 64 bit unsigned packet number, which is 32 | used as a part of a cryptographic nonce for packet encryption. 33 | payload: The 1-RTT packet payload. 34 | """ 35 | @type t :: %__MODULE__{ 36 | c: integer, 37 | k: integer, 38 | type: integer, 39 | connection_id: integer, 40 | packet_number: integer, 41 | payload: bitstring 42 | } 43 | 44 | @enforce_keys [:c, :k, :type, :packet_number, :payload] 45 | defstruct [:c, :k, :type, :connection_id, :packet_number, :payload] 46 | 47 | defmodule Type do 48 | @moduledoc """ 49 | This module contains the types for all different 50 | packets that can be sent with a short header. 51 | 52 | The remaining 5 bits of octet 0 include one of 32 packet types. 53 | 54 | [Source](https://quicwg.github.io/base-drafts/draft-ietf-quic-transport.html#short-packet-types) 55 | """ 56 | 57 | @doc """ 58 | 0x01 1 octet. 59 | """ 60 | @spec one_octet() :: 1 61 | def one_octet(), do: 1 62 | 63 | @doc """ 64 | 0x02 2 octet. 65 | """ 66 | @spec two_octet() :: 2 67 | def two_octet(), do: 2 68 | 69 | @doc """ 70 | 0x03 4 octet. 71 | """ 72 | @spec four_octet() :: 3 73 | def four_octet(), do: 3 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/quic/packet/packet.ex: -------------------------------------------------------------------------------- 1 | defmodule QUIC.Packet do 2 | @moduledoc """ 3 | This module is used as a facade that delegates to the proper 4 | functions. This is only a convenience module that allows for 5 | better naming and calling order for each function used to 6 | encode, or decode a packet. 7 | """ 8 | 9 | ## Header dispatches 10 | 11 | defdelegate header(type, connection_id, packet_number, version, payload), 12 | to: QUIC.Header.Long, as: :encode 13 | 14 | ## Packet dispatches 15 | 16 | defdelegate version_negotiation(), 17 | to: QUIC.Packet.VersionNegotiation, as: :encode 18 | end 19 | -------------------------------------------------------------------------------- /lib/quic/packet/version_negotiation.ex: -------------------------------------------------------------------------------- 1 | defmodule QUIC.Packet.VersionNegotiation do 2 | @moduledoc """ 3 | A Version Negotiation packet has long headers with a type value of 0x01 4 | and is sent only by servers. The Version Negotiation packet is a response 5 | to a client packet that contains a version that is not supported 6 | by the server. 7 | 8 | ``` 9 | 0 1 2 3 10 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 11 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 12 | | Supported Version 1 (32) ... 13 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 14 | | [Supported Version 2 (32)] ... 15 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 16 | ... 17 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 18 | | [Supported Version N (32)] ... 19 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 20 | ``` 21 | 22 | [Source](https://quicwg.github.io/base-drafts/draft-ietf-quic-transport.html#packet-version) 23 | """ 24 | 25 | @typedoc """ 26 | version: A list of available versions 27 | """ 28 | @type t :: %__MODULE__{ 29 | versions: [integer] 30 | } 31 | 32 | @enforce_keys [:versions] 33 | defstruct [:versions] 34 | 35 | @doc """ 36 | Decodes the QUIC versions from the Version Negotiation 37 | packet and returns it in a form of this module type. 38 | 39 | The supported versions are contained in 4 byte chunks beginning 40 | at the 9th zero-indexed byte and continuing to the end of the packet. 41 | 42 | ## Parameters 43 | 44 | - packet: A bitstring representing a version negotiation packet. 45 | 46 | ## Examples 47 | 48 | iex> QUIC.Packet.VersionNegotiation.decode(<<0, 0, 0, 40>>) 49 | %QUIC.Packet.VersionNegotiation{versions: [40]} 50 | """ 51 | @spec decode(bitstring) :: __MODULE__.t 52 | def decode(packet) when is_bitstring(packet) do 53 | list = 54 | packet 55 | |> :binary.bin_to_list() 56 | |> Enum.chunk(4) 57 | |> Enum.map(fn(x) -> :erlang.list_to_bitstring(x) end) 58 | |> List.foldr([], fn(x, acc) -> 59 | <> = x 60 | [version | acc] 61 | end) 62 | 63 | %__MODULE__{versions: list} 64 | end 65 | 66 | @doc """ 67 | Encodes the supported QUIC versions by this server in to bitstring form. 68 | 69 | ## Examples 70 | 71 | iex> QUIC.Packet.VersionNegotiation.encode() 72 | <<0, 0, 0, 40>> 73 | """ 74 | @spec encode() :: bitstring 75 | def encode() do 76 | QUIC.supported_versions() 77 | |> Enum.reduce(<<>>, fn(x, acc) -> acc <> <> end) 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/quic/util/bitstring_helpers.ex: -------------------------------------------------------------------------------- 1 | defprotocol QUIC.Type do 2 | @moduledoc """ 3 | Various extension functions for Bitstring since the QUIC 4 | library deals with bitstrings structure a lot. 5 | """ 6 | 7 | alias QUIC.Header 8 | 9 | @fallback_to_any true 10 | 11 | @doc """ 12 | Checks the type of the Packet for the given bitstring. 13 | """ 14 | @spec packet_type(bitstring) :: {Header.Long, integer} | {Header.Short, integer} | {:error, String.t} 15 | def packet_type(bitstring) 16 | end 17 | 18 | defimpl QUIC.Type, for: Bitstring do 19 | 20 | alias QUIC.Header.Long 21 | alias QUIC.Header.Short 22 | 23 | ## QUIC.Header.Long.Type 24 | 25 | def packet_type(<<1::1, 1::7, _::bitstring>>), do: {Long, Long.Type.version_negotiation()} 26 | 27 | def packet_type(<<1::1, 2::7, _::bitstring>>), do: {Long, Long.Type.client_initial()} 28 | 29 | def packet_type(<<1::1, 3::7, _::bitstring>>), do: {Long, Long.Type.server_stateless_retry()} 30 | 31 | def packet_type(<<1::1, 4::7, _::bitstring>>), do: {Long, Long.Type.server_cleartext()} 32 | 33 | def packet_type(<<1::1, 5::7, _::bitstring>>), do: {Long, Long.Type.client_cleartext()} 34 | 35 | def packet_type(<<1::1, 6::7, _::bitstring>>), do: {Long, Long.Type.zero_rtt_protected()} 36 | 37 | ## QUIC.Header.Short.Type 38 | 39 | def packet_type(<<0::1, _::1, _::1, 1::7, _::bitstring>>), do: {Short, Short.Type.one_octet()} 40 | 41 | def packet_type(<<0::1, _::1, _::1, 2::7, _::bitstring>>), do: {Short, Short.Type.two_octet()} 42 | 43 | def packet_type(<<0::1, _::1, _::1, 3::7, _::bitstring>>), do: {Short, Short.Type.four_octet()} 44 | end 45 | 46 | defimpl QUIC.Type, for: Any do 47 | 48 | ## TODO: Return a real error 49 | 50 | def packet_type(_), do: {:error, "No match for packet type"} 51 | end 52 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule QUIC.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :quic, 7 | version: "0.1.0", 8 | elixir: "~> 1.5", 9 | start_permanent: Mix.env == :prod, 10 | deps: deps(), 11 | dialyzer: [ 12 | flags: [:error_handling, :race_conditions, :underspecs], 13 | paths: ["_build/dev/lib/quic/ebin"], 14 | ignore_warnings: "dialyzer.ignore-warnings" 15 | ] 16 | ] 17 | end 18 | 19 | # Run "mix help compile.app" to learn about applications. 20 | def application do 21 | [ 22 | extra_applications: [:logger] 23 | ] 24 | end 25 | 26 | # Run "mix help deps" to learn about dependencies. 27 | defp deps do 28 | [ 29 | {:ex_doc, "~> 0.18.1"}, 30 | {:dialyxir, "~> 0.5.1", only: [:dev], runtime: false}, 31 | {:credo, "~> 0.8.8", only: [:dev, :test], runtime: false} 32 | ] 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 2 | "credo": {:hex, :credo, "0.8.8", "990e7844a8d06ebacd88744a55853a83b74270b8a8461c55a4d0334b8e1736c9", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, 3 | "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, 4 | "earmark": {:hex, :earmark, "1.2.3", "206eb2e2ac1a794aa5256f3982de7a76bf4579ff91cb28d0e17ea2c9491e46a4", [:mix], [], "hexpm"}, 5 | "ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}} 6 | -------------------------------------------------------------------------------- /test/quic/connection_test.exs: -------------------------------------------------------------------------------- 1 | defmodule QUIC.ConnectionTest do 2 | use ExUnit.Case 3 | 4 | doctest QUIC.Connection 5 | 6 | test "Start a QUIC connection with start" do 7 | {:ok, pid} = QUIC.Connection.start() 8 | assert Process.alive?(pid) 9 | 10 | {:ok, socket} = QUIC.Connection.open(pid, 1337) 11 | assert is_port(socket) 12 | 13 | assert QUIC.Connection.open(pid, 1337) == {:error, :eaddrinuse} 14 | assert QUIC.Connection.close(pid) == :ok 15 | 16 | # Give GenServer some time to exit 17 | :timer.sleep(10) 18 | 19 | assert !Process.alive?(pid) 20 | end 21 | 22 | test "Start a QUIC connection with start_link" do 23 | {:ok, pid} = QUIC.Connection.start_link() 24 | assert Process.alive?(pid) 25 | 26 | {:ok, socket} = QUIC.Connection.open(pid, 1337) 27 | assert is_port(socket) 28 | 29 | # Trapping an exit since we're using start_link 30 | # and we don't want to take the test process down with us 31 | Process.flag(:trap_exit, true) 32 | 33 | assert QUIC.Connection.close(pid) == :ok 34 | 35 | # Give GenServer some time to exit 36 | :timer.sleep(10) 37 | 38 | Process.flag(:trap_exit, false) 39 | 40 | assert !Process.alive?(pid) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/quic/frame/frame_types_test.exs: -------------------------------------------------------------------------------- 1 | defmodule QUIC.Frame.TypeTest do 2 | use ExUnit.Case 3 | 4 | doctest QUIC.Frame.Type 5 | 6 | alias QUIC.Frame.Type 7 | 8 | test "Stream frame type values" do 9 | ## No additional bits set 10 | assert Type.stream(false, false, false) == 0x10 11 | 12 | ## FIN bit set 13 | assert Type.stream(true, false, false) == 0x11 14 | 15 | ## LEN bit set 16 | assert Type.stream(false, true, false) == 0x12 17 | 18 | ## FIN and LEN bits set 19 | assert Type.stream(true, true, false) == 0x13 20 | 21 | ## OFF bit set 22 | assert Type.stream(false, false, true) == 0x14 23 | 24 | ## FIN and OFF bits set 25 | assert Type.stream(true, false, true) == 0x15 26 | 27 | ## LEN and OFF bit set 28 | assert Type.stream(false, true, true) == 0x16 29 | 30 | ## FIN, LEN and OFF bits set 31 | assert Type.stream(true, true, true) == 0x17 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/quic/packet/version_negotiation_test.exs: -------------------------------------------------------------------------------- 1 | defmodule QUIC.Packet.VersionNegotiationTest do 2 | use ExUnit.Case 3 | 4 | doctest QUIC.Packet.VersionNegotiation 5 | 6 | alias QUIC.Packet.VersionNegotiation 7 | 8 | test "Encode" do 9 | assert <<40::32>> == VersionNegotiation.encode() 10 | end 11 | 12 | test "Decode" do 13 | packet = <<39::32, 40::32, 41::32>> 14 | expected = %VersionNegotiation{versions: [39, 40, 41]} 15 | 16 | assert VersionNegotiation.decode(packet) == expected 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/quic_test.exs: -------------------------------------------------------------------------------- 1 | defmodule QUICTest do 2 | use ExUnit.Case 3 | 4 | doctest QUIC 5 | end 6 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------