Skip to content

Conversation

Munksgaard
Copy link

This commit includes a very rudimentary NIF-based wrapper for interfacing with libtailscale from Elixir/Erlang.

It includes everything from tailscale.h, except
tailscale_enable_funnel_to_localhost_plaintext_http1. However only the listen/accept parts have been tested somewhat thoroughly.

An example echo server can be run by first executing mix deps.get and then mix run examples/echo.exs.

This Elixir library (which is also published to
Hex, the Elixir package manager), but it should probably not be used directly. Instead, I have provided a gen_tailscale library, which wraps the libtailscale sockets in a gen_tcp-like interface. I've also released 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 hacking the original 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 is able to accept connections from different Tailscale users and show their username by retrieving data from the Tailscale connection.

Finally, I should note that I'm not sure merging this elixir-wrapper directly into this repository is the best way forward. I've published a package based on my fork, because Hex, the Elixir package manager, requires other packages' dependencies to also be Hex-packages. But if we merge this PR, should the package then point to tailscale/libtailscale? That seems weird, since I'm the one who has released the package. Anyway, let me hear your thoughts. I wanted to share my work here, but I'm open for other suggestions for how to actually proceed.

This commit includes a very rudimentary NIF-based wrapper for interfacing with
`libtailscale` from Elixir/Erlang.

It includes everything from `tailscale.h`, except
`tailscale_enable_funnel_to_localhost_plaintext_http1`. However only the
listen/accept parts have been tested somewhat thoroughly.

An example echo server can be run by first executing `mix deps.get` and then
`mix run examples/echo.exs`.

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, I have provided a
[`gen_tailscale`](https://hex.pm/packages/gen_tailscale) library, which wraps
the libtailscale sockets in a `gen_tcp`-like interface. I've also released
[`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 hacking the original
`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.
@ghost
Copy link

ghost commented Jun 9, 2025

Pull Request Revisions

RevisionDescription
r1
Added Elixir library for TailscaleCreated a complete Elixir library project structure for Libtailscale, including Makefile, NIF bindings, mix configuration, README, example code, and necessary project files for an Elixir NIF wrapper around libtailscale

✅ AI review completed for r1
Help React with emojis to give feedback on AI-generated reviews:
  • 👍 means the feedback was helpful and actionable
  • 👎 means the feedback was incorrect or unhelpful
💬 Replying to feedback with a comment helps us improve the system. Your input also contributes to shaping future interactions with the AI reviewer.

We'd love to hear from you—reach out anytime at [email protected].


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'.

Comment on lines +11 to +17
static char* binary_to_string(ErlNifBinary* bin) {
char* s = malloc(bin->size * sizeof(char) + 1);
memcpy(s, (const char*)bin->data, bin->size);
s[bin->size] = '\0';

return s;
}
Copy link

Choose a reason for hiding this comment

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

In elixir/native/libtailscale_nif.c, the binary_to_string function allocates memory but in some error paths this memory isn't freed. Consider adding error handling that ensures allocated memory is always freed, especially in functions like tailscale_set_dir_nif.

Comment on lines +475 to +477
// Read failed
return enif_make_badarg(env);
} else {
Copy link

Choose a reason for hiding this comment

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

In elixir/native/libtailscale_nif.c, the tailscale_read_nif function (lines 441-481) returns enif_make_badarg(env) for all read failures without capturing the actual error. Consider returning the specific error with return_errmsg() as done in other functions for better error diagnosis.

Comment on lines +502 to +505
if ((ret = write(conn, binary.data, binary.size)) < 0) {
// An error occurred
return enif_make_badarg(env);
}
Copy link

Choose a reason for hiding this comment

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

In elixir/native/libtailscale_nif.c, the tailscale_write_nif function (lines 483-508) returns enif_make_badarg(env) for write failures without capturing the actual error reason. Consider using return_errmsg() similar to other functions.

Comment on lines +523 to +547
if(close(conn) != 0) {
// An error occurred
return enif_make_badarg(env);
}

return atom_ok;
}

static ERL_NIF_TERM tailscale_close_listener_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[])
{
tailscale sd;
tailscale_listener listener;

if(!enif_get_int(env, argv[0], &sd)) {
return enif_make_badarg(env);
}

if(!enif_get_int(env, argv[1], &listener)) {
return enif_make_badarg(env);
}

if(close(listener) != 0) {
// An error occurred
return enif_make_badarg(env);
}
Copy link

Choose a reason for hiding this comment

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

In elixir/native/libtailscale_nif.c, the close functions (tailscale_close_connection_nif and tailscale_close_listener_nif) should check errno when close() fails and use return_errmsg() to provide detailed error information instead of just returning enif_make_badarg(env).

Comment on lines +25 to +30
{:ok, socket} = :socket.open(socket_fd)

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

:ok = :socket.send(socket, s)
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.

@elliotblackburn
Copy link

elliotblackburn commented Jun 28, 2025

This is really great to see! I actually had a similar version I was working on as well but it looks like you got quite a bit further than me, especially with the gen and transport versions 👏

Finally, I should note that I'm not sure merging this elixir-wrapper directly into this repository is the best way forward

I'm not a tailscalar, but I am an insider so I'll see if I can get someone to take a look and comment on how they see this repo progressing with the other existing bindings. That might help figure out what options are available and whether a merge would have any benefit.

Having some blessed version of the bindings in this repo might garner a little more support from tailscale and the community. On the other hand, I don't believe the Tailscale team have much in the way of elixir experience, that being the case it'll probably fall to the community to maintain in some capacity anyway. That's not to say they couldn't support that in other ways though!

I'm really excited to see how far you've managed to get this. I'll pull your branch and packages and will give it a go and see if I can get a demo working as well. Maybe I can contribute some fixes I find along the way.

@Munksgaard
Copy link
Author

Thanks @elliotblackburn! I spent way too much time scratching this particular itch, but it was nice to finally get something working. I would very much appreciate if you could try it out and see if you can get this and the companion libraries working.

It would be great to have some kind of statement from Tailscale so we can figure out how this library could move forward. If they don't want it to be part of this repo, an alternative could be to have a separate repo with a git submodule that points to this repo. I think that could work.

For reference, I wrote a bit about my efforts here: https://gist.github.com/Munksgaard/9102f0be2562f7ba1eca32b7e0da643e

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants