-
Notifications
You must be signed in to change notification settings - Fork 33
elixir: Initial commit #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
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.
Pull Request Revisions
✅ AI review completed for r1 HelpReact with emojis to give feedback on AI-generated reviews:
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 |
There was a problem hiding this comment.
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'.
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; | ||
} |
There was a problem hiding this comment.
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.
// Read failed | ||
return enif_make_badarg(env); | ||
} else { |
There was a problem hiding this comment.
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.
if ((ret = write(conn, binary.data, binary.size)) < 0) { | ||
// An error occurred | ||
return enif_make_badarg(env); | ||
} |
There was a problem hiding this comment.
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.
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); | ||
} |
There was a problem hiding this comment.
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).
{:ok, socket} = :socket.open(socket_fd) | ||
|
||
# Now echo one message | ||
{:ok, s} = :socket.recv(socket) | ||
|
||
:ok = :socket.send(socket, s) |
There was a problem hiding this comment.
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.
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 👏
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. |
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 |
This commit includes a very rudimentary NIF-based wrapper for interfacing with
libtailscale
from Elixir/Erlang.It includes everything from
tailscale.h
, excepttailscale_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 thenmix 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 agen_tcp
-like interface. I've also releasedtailscale_transport
, which allows users to expose their bandit/phoenix-based app directly to their tailnet usinglibtailscale
and thegen_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 originalgen_tcp
module to uselibtailscale
and could use a total rewrite at some point. However, it works well enough that my example applicationtschat
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.