Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions elixir/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
29 changes: 29 additions & 0 deletions elixir/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
ex_tailscale-*.tar

# Temporary files, for example, from tests.
/tmp/

# Object files
*.o
5 changes: 5 additions & 0 deletions elixir/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## Libtailscale v0.1.0

The initial release of `Libtailscale`.
79 changes: 79 additions & 0 deletions elixir/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Libtailscale

Thin NIF-wrapper around
[libtailscale](https://github.com/tailscale/libtailscale/).

> #### Warning {: .warning}
>
> Should not be used directly. Use
> [`gen_tailscale`](https://hex.pm/packages/gen_tailscale) or
> [`TailscaleTransport`](https://hex.pm/packages/tailscale_transport) instead.


## Dependencies

Building this package requires access to a Go compiler as well as GCC.

## Usage

There's one working example in `examples/echo.exs`. To run, first run `mix
deps.get` and then `mix run examples/echo.exs`. You will need a
[Tailscale](https://tailscale.com/) account. The first time you run the example,
it will ask you to log in by following a link. Alternatively, here is the
simplest possible echo server:

```elixir
# Create a new Tailscale server object.
ts = Libtailscale.new()

# Set the Tailscale connection to be ephemeral.
:ok = Libtailscale.set_ephemeral(ts, 1)
:ok = Libtailscale.set_hostname(ts, "libtailscale-echo")

:ok = Libtailscale.up(ts)

# Create a listener socket using the NIF.
{:ok, listener_fd} = Libtailscale.listen(ts, "tcp", ":1999")

{:ok, listener_socket} = :socket.open(listener_fd)

# Customer "accept" functionality
{:ok, cmsg} = :socket.recvmsg(listener_socket)
<<socket_fd::integer-native-32>> = hd(cmsg.ctrl).data

{:ok, socket} = :socket.open(socket_fd)

# Now echo one message
{:ok, s} = :socket.recv(socket)
:ok = :socket.send(socket, s)

# And clean up
:socket.shutdown(socket, :read_write)
:socket.close(socket)
:socket.close(listener_socket)
```

After running the server (wait for the "state is Running" message), simply use
`telnet libtailscale-echo 1999` in another terminal to connect to it over the
tailnet. The server that's running is a simple echo server that will wait for a
single line of input, return it to the client and then close the connection.

## Warning

This Elixir library (which is also published to
[Hex](https://hex.pm/packages/libtailscale), the Elixir package manager), but it
should probably not be used directly. Instead, the
[`gen_tailscale`](https://hex.pm/packages/gen_tailscale) library should be used,
which wraps the libtailscale sockets in a `gen_tcp`-like interface. There's also
also [`tailscale_transport`](https://hex.pm/packages/tailscale_transport), which
allows users to expose their bandit/phoenix-based app directly to their tailnet
using `libtailscale` and the `gen_tailscale` wrapper.

Everything in this chain of packages should be considered proof of concept at
this point and should not be used for anything important. Especially the
`gen_tailscale` library has been constructed by crudely hacing the original
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a typo in the README.md on line 74: 'crudely hacing' should be 'crudely hacking'.

`gen_tcp` module to use `libtailscale` and could use a total rewrite at some
point. However, it works well enough that my example application
[`tschat`](https://github.com/Munksgaard/tschat) is able to accept connections
from different Tailscale users and show their username by retrieving data from
the Tailscale connection.
37 changes: 37 additions & 0 deletions elixir/examples/echo.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Create a new Tailscale server object.
ts = Libtailscale.new()

# Set the Tailscale connection to be ephemeral.
:ok = Libtailscale.set_ephemeral(ts, 1)
:ok = Libtailscale.set_hostname(ts, "libtailscale-echo")

:ok = Libtailscale.up(ts)

{:ok, ips} = Libtailscale.getips(ts)
IO.puts("Server IPs: #{ips}")

# Create a listener socket using the NIF.
{:ok, listener_fd} = Libtailscale.listen(ts, "tcp", ":1999")

{:ok, listener_socket} = :socket.open(listener_fd)

# Customer "accept" functionality
{:ok, cmsg} = :socket.recvmsg(listener_socket)
<<socket_fd::integer-native-32>> = hd(cmsg.ctrl).data

{:ok, remoteaddr} = Libtailscale.getremoteaddr(ts, listener_fd, socket_fd)
IO.puts("Client IP: #{remoteaddr}")

{:ok, socket} = :socket.open(socket_fd)

# Now echo one message
{:ok, s} = :socket.recv(socket)

:ok = :socket.send(socket, s)
Comment on lines +25 to +30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In elixir/examples/echo.exs, there's no error handling for network operations. Consider adding proper error handling for socket operations like :socket.recv and :socket.send, especially since this is meant to be an educational example.


# And clean up
:socket.shutdown(socket, :read_write)

:socket.close(socket)

:socket.close(listener_socket)
27 changes: 27 additions & 0 deletions elixir/flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions elixir/flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; };

outputs = inputs:
let
supportedSystems =
[ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
forEachSupportedSystem = f:
inputs.nixpkgs.lib.genAttrs supportedSystems
(system: f { pkgs = import inputs.nixpkgs { inherit system; }; });

in {
devShells = forEachSupportedSystem ({ pkgs }: {
default = pkgs.mkShell {
packages =
[ pkgs.elixir pkgs.go pkgs.gnumake pkgs.gcc ];
};
});
};
}
109 changes: 109 additions & 0 deletions elixir/lib/libtailscale.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
defmodule Libtailscale do
@on_load :init

@appname :libtailscale
@libname "libtailscale"

def init do
so_name =
case :code.priv_dir(@appname) do
{:error, :bad_name} ->
case File.dir?(Path.join(["..", :priv])) do
true ->
Path.join(["..", :priv, @libname])

_ ->
Path.join([:priv, @libname])
end

dir ->
Path.join(dir, @libname)
end

:erlang.load_nif(so_name, 0)
end

def new() do
not_loaded(:new)
end

def start(_ts) do
not_loaded(:start)
end

def up(_ts) do
not_loaded(:up)
end

def close(_ts) do
not_loaded(:close)
end

def set_dir(_ts, _dir) do
not_loaded(:set_dir)
end

def set_hostname(_ts, _hostname) do
not_loaded(:set_hostname)
end

def set_authkey(_ts, _authkey) do
not_loaded(:set_authkey)
end

def set_control_url(_ts, _control_url) do
not_loaded(:set_control_url)
end

def set_ephemeral(_ts, _ephemeral) do
not_loaded(:set_ephemeral)
end

def set_logfd(_ts, _logfd) do
not_loaded(:set_logfd)
end

def getips(_ts) do
not_loaded(:getips)
end

def dial(_ts, _network, _addr) do
not_loaded(:dial)
end

def listen(_ts, _network, _addr) do
not_loaded(:listen)
end

def getremoteaddr(_ts, _listener, _conn) do
not_loaded(:getremoteaddr)
end

def accept(_ts, _listener) do
not_loaded(:accept)
end

def read(_ts, _conn, _len) do
not_loaded(:read)
end

def write(_ts, _conn, _bin) do
not_loaded(:write)
end

def close_connection(_ts, _conn) do
not_loaded(:close_connection)
end

def close_listener(_ts, _conn) do
not_loaded(:close_listener)
end

def loopback(_ts) do
not_loaded(:loopback)
end

defp not_loaded(line) do
:erlang.nif_error({:not_loaded, [{:module, __MODULE__}, {:function, line}]})
end
end
Loading