-
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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}"] | ||
] |
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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Changelog | ||
|
||
## Libtailscale v0.1.0 | ||
|
||
The initial release of `Libtailscale`. |
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 | ||
`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. |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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 ]; | ||
}; | ||
}); | ||
}; | ||
} |
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 |
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'.