tailscale 0.3.3

A work-in-progress Tailscale implementation
Documentation
defmodule Tailscale do
  @moduledoc """
  Elixir bindings for the Tailscale Rust client.

  ## Nomenclature (devices, peers, nodes, etc.)

  In our parlance, anything that shows up on console.tailscale.com
  and gets a tailnet IP is known canonically as a "device", though these are also variously been
  referred to as "nodes" or "peers". Conventionally, each of these would be a device running
  `tailscaled`, but with the advent of `tsnet` and now `tailscale-rs` and its derivative
  cross-language clients, a single computer can have many Tailscale connections simultaneously,
  possibly to many different tailnets. As an attempt to capture the whole ontology of "things that
  have a persistent identity and tailnet IP", we try to refer to them uniformly by the umbrella term
  "device".
  """

  @typedoc """
  An IPv4 address.
    
  `tailscale` is capable of interpreting either the `m::inet` format or a `String`.
  """
  @type ip4_addr() :: :inet.ip4_address() | String.t()

  @typedoc """
  An IPv6 address.
    
  `tailscale` is capable of interpreting either the `m::inet` format or a `String`.
  """
  @type ip6_addr() :: :inet.ip6_address() | String.t()

  @typedoc """
  An IP address (v4 or v6).
    
  `tailscale` is capable of interpreting either the `m::inet` format or a `String`.
  """
  @type ip_addr() :: ip4_addr() | ip6_addr()

  @typedoc """
  Handle to a tailscale "device", i.e. a unique tailnet-connected identity with a network address.
  See the note in `connect/2` about nomenclature for more details.
  """
  @opaque t() :: Tailscale.Native.device()

  @typedoc """
  Options for connecting to Tailscale:

  - `auth_key`: the auth key to use to authorize this device. You only need to supply this if the
    device's keys aren't authorized.
  - `keys`: the `m:Tailscale.Keystate` to use to connect. This defines the device identity.
  - `hostname`: the hostname this device will request. If omitted, uses the hostname the OS reports.
  - `tags`: tags the device will request.
  - `control_url`: the url of the control server to use.
  """
  @type options :: [
          auth_key: String.t(),
          keys: Tailscale.Keystate.t(),
          control_url: String.t(),
          hostname: String.t(),
          tags: [String.t()]
        ]

  @spec connect(String.t(), options()) :: {:ok, t()} | {:error, any()}
  @doc """
  Open a connection to tailscale, creating a device connected to a tailnet. Loads key state from
  the given path, creating it if it doesn't exist.

  See `t:options/0` for details on available options.
  """
  def connect(key_file_path, options) when is_binary(key_file_path) do
    case Tailscale.Native.load_key_file(key_file_path) do
      {:ok, keys} ->
        Keyword.put(options, :keys, keys) |> connect()

      err ->
        err
    end
  end

  @spec connect(options() | String.t()) :: {:ok, t()} | {:error, any()}
  @doc """
  Open a connection to Tailscale, creating a device connected to a tailnet. If the argument is a
  `m:String`, this is equivalent to `connect/2` with an empty option list.

  See `t:options/0` for details on available options. You may want to call `connect/2` for an easier
  way to load key state from a file.
  """
  def connect(options \\ [])

  def connect(options) when is_list(options),
    do: :proplists.to_map(options) |> Tailscale.Native.connect()

  def connect(key_file_path) when is_binary(key_file_path), do: connect(key_file_path, [])

  @spec ipv4_addr(t()) :: {:ok, :inet.ip4_address()} | {:error, any()}
  @doc """
  Get the current IPv4 address of this Tailscale node.
    
  Blocks until the address is available.
  """
  def ipv4_addr(dev), do: Tailscale.Native.ipv4_addr(dev)

  @spec ipv6_addr(t()) :: {:ok, :inet.ip6_address()} | {:error, any()}
  @doc """
  Get the current IPv6 address of this Tailscale node.
    
  Blocks until the address is available.

  Note that this address is in `t::inet.ip6_address/0` format (16-bit segments), which may be
  difficult to read. See `:inet.ntoa/1` to format to a string.
  """
  def ipv6_addr(dev), do: Tailscale.Native.ipv6_addr(dev)

  @spec self_node(t()) :: {:ok, Tailscale.NodeInfo.t()} | {:error, any()}
  @doc """
  Get this node's `m:Tailscale.NodeInfo`.
  """
  defdelegate self_node(dev), to: Tailscale.Native

  @spec peer_by_name(t(), String.t()) :: {:ok, Tailscale.NodeInfo.t() | nil} | {:error, any()}
  @doc """
  Look up a peer by name.

  Returns `{:ok, nil}` if there was no such peer, and `{:error, reason}` if the lookup encountered
  an error.
  """
  def peer_by_name(dev, name), do: Tailscale.Native.peer_by_name(dev, name)

  @spec peer_by_tailnet_ip(t(), Tailscale.ip_addr()) ::
          {:ok, Tailscale.NodeInfo.t() | nil} | {:error, any()}
  @doc """
  Look up the peer with the given tailnet IP address.

  Returns `{:ok, nil}` if there was no such peer. `:error` if the lookup encountered an error.
  """
  defdelegate peer_by_tailnet_ip(dev, ip), to: Tailscale.Native

  @spec peers_with_route(t(), Tailscale.ip_addr()) ::
          {:ok, [Tailscale.NodeInfo.t()]} | {:error, any()}
  @doc """
  Retrieve the most narrow set of peers that accept packets for the specified IP.
  """
  defdelegate peers_with_route(dev, ip), to: Tailscale.Native
end