From 75bfd65d2f722b7c3a14cbf96feb9021eb01215e Mon Sep 17 00:00:00 2001 From: Philip Munksgaard Date: Thu, 1 May 2025 20:38:07 +0200 Subject: [PATCH] elixir: Initial commit 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. --- elixir/.formatter.exs | 4 + elixir/.gitignore | 29 ++ elixir/CHANGELOG.md | 5 + elixir/README.md | 79 ++++ elixir/examples/echo.exs | 37 ++ elixir/flake.lock | 27 ++ elixir/flake.nix | 20 + elixir/lib/libtailscale.ex | 109 ++++++ elixir/mix.exs | 99 +++++ elixir/mix.lock | 11 + elixir/native/Makefile | 82 ++++ elixir/native/libtailscale_nif.c | 635 +++++++++++++++++++++++++++++++ elixir/priv/.gitignore | 2 + 13 files changed, 1139 insertions(+) create mode 100644 elixir/.formatter.exs create mode 100644 elixir/.gitignore create mode 100644 elixir/CHANGELOG.md create mode 100644 elixir/README.md create mode 100644 elixir/examples/echo.exs create mode 100644 elixir/flake.lock create mode 100644 elixir/flake.nix create mode 100644 elixir/lib/libtailscale.ex create mode 100644 elixir/mix.exs create mode 100644 elixir/mix.lock create mode 100644 elixir/native/Makefile create mode 100644 elixir/native/libtailscale_nif.c create mode 100644 elixir/priv/.gitignore diff --git a/elixir/.formatter.exs b/elixir/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/elixir/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/elixir/.gitignore b/elixir/.gitignore new file mode 100644 index 0000000..8e0456f --- /dev/null +++ b/elixir/.gitignore @@ -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 \ No newline at end of file diff --git a/elixir/CHANGELOG.md b/elixir/CHANGELOG.md new file mode 100644 index 0000000..70d73d5 --- /dev/null +++ b/elixir/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Libtailscale v0.1.0 + +The initial release of `Libtailscale`. diff --git a/elixir/README.md b/elixir/README.md new file mode 100644 index 0000000..34b1c46 --- /dev/null +++ b/elixir/README.md @@ -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) +<> = 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. diff --git a/elixir/examples/echo.exs b/elixir/examples/echo.exs new file mode 100644 index 0000000..e4228b9 --- /dev/null +++ b/elixir/examples/echo.exs @@ -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) +<> = 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) + +# And clean up +:socket.shutdown(socket, :read_write) + +:socket.close(socket) + +:socket.close(listener_socket) diff --git a/elixir/flake.lock b/elixir/flake.lock new file mode 100644 index 0000000..21ff2be --- /dev/null +++ b/elixir/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1748190013, + "narHash": "sha256-R5HJFflOfsP5FBtk+zE8FpL8uqE7n62jqOsADvVshhE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "62b852f6c6742134ade1abdd2a21685fd617a291", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/elixir/flake.nix b/elixir/flake.nix new file mode 100644 index 0000000..6ce7140 --- /dev/null +++ b/elixir/flake.nix @@ -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 ]; + }; + }); + }; +} diff --git a/elixir/lib/libtailscale.ex b/elixir/lib/libtailscale.ex new file mode 100644 index 0000000..9cfa22d --- /dev/null +++ b/elixir/lib/libtailscale.ex @@ -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 diff --git a/elixir/mix.exs b/elixir/mix.exs new file mode 100644 index 0000000..021cdbe --- /dev/null +++ b/elixir/mix.exs @@ -0,0 +1,99 @@ +defmodule ExTailscale.MixProject do + use Mix.Project + + @version "0.1.0" + + def project do + [ + app: :libtailscale, + version: @version, + elixir: "~> 1.17", + make_cwd: "native", + make_clean: ["clean"], + compilers: [:elixir_make] ++ Mix.compilers(), + source_url: "https://github.com/Munksgaard/libtailscale/tree/elixir/elixir", + deps: deps(), + description: description(), + package: package(), + docs: docs(), + aliases: aliases() + ] + end + + # Run "mix help compile.app" to learn about applications. + def application do + [ + extra_applications: [:logger] + ] + end + + # Run "mix help deps" to learn about dependencies. + defp deps do + [ + # Development dependencies + {:elixir_make, "~> 0.9.0", runtime: false}, + {:ex_doc, "~> 0.38", only: :dev, runtime: false, warn_if_outdated: true} + ] + end + + defp description do + """ + Thin NIF-wrapper around libtailscale. Should not be used directly. + """ + end + + defp package do + [ + maintainers: ["Philip Munksgaard"], + licenses: ["BSD-3-Clause"], + links: links(), + files: [ + "lib", + "native/Makefile", + "native/libtailscale_nif.c", + "mix.exs", + "README*", + "CHANGELOG*", + "priv/libtailscale/go.mod", + "priv/libtailscale/go.sum", + "priv/libtailscale/tailscale.c", + "priv/libtailscale/tailscale.h", + "priv/libtailscale/tailscale.go", + "priv/libtailscale/Makefile", + "priv/libtailscale/LICENSE" + ] + ] + end + + def links do + %{ + "GitHub" => "https://github.com/Munksgaard/libtailscale/tree/elixir" + } + end + + defp docs do + [ + main: "readme", + extras: [ + "README.md", + "priv/libtailscale/LICENSE", + "CHANGELOG.md" + ], + formatters: ["html"], + skip_undefined_reference_warnings_on: ["changelog", "CHANGELOG.md"] + ] + end + + defp copy_native_files do + [ + "cmd mkdir -p priv/libtailscale", + "cmd cp -p ../go.mod ../go.sum ../tailscale.c ../tailscale.h ../tailscale.go ../Makefile ../LICENSE priv/libtailscale" + ] + end + + defp aliases do + [ + "deps.get": copy_native_files() ++ ["deps.get"] + ] + end +end diff --git a/elixir/mix.lock b/elixir/mix.lock new file mode 100644 index 0000000..98eaa4d --- /dev/null +++ b/elixir/mix.lock @@ -0,0 +1,11 @@ +%{ + "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, + "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, + "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, + "thousand_island": {:hex, :thousand_island, "1.3.13", "d598c609172275f7b1648c9f6eddf900e42312b09bfc2f2020358f926ee00d39", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5a34bdf24ae2f965ddf7ba1a416f3111cfe7df50de8d66f6310e01fc2e80b02a"}, +} diff --git a/elixir/native/Makefile b/elixir/native/Makefile new file mode 100644 index 0000000..a1094ff --- /dev/null +++ b/elixir/native/Makefile @@ -0,0 +1,82 @@ +# Based on c_src.mk from erlang.mk by Loic Hoguin + +CURDIR := $(shell pwd) +BASEDIR := $(abspath $(CURDIR)/..) + +LIBTAILSCALEDIR ?= $(abspath $(BASEDIR)/priv/libtailscale) + +PROJECT ?= $(notdir $(LIBTAILSCALEDIR)) +PROJECT := $(strip $(PROJECT)) + +ERTS_INCLUDE_DIR ?= $(shell erl -noshell -eval "io:format(\"~ts/erts-~ts/include/\", [code:root_dir(), erlang:system_info(version)])." -s init stop) +ERL_INTERFACE_INCLUDE_DIR ?= $(shell erl -noshell -eval "io:format(\"~ts\", [code:lib_dir(erl_interface, include)])." -s init stop) +ERL_INTERFACE_LIB_DIR ?= $(shell erl -noshell -eval "io:format(\"~ts\", [code:lib_dir(erl_interface, lib)])." -s init stop) +LIBTAILSCALE_LIB_DIR ?= $(LIBTAILSCALEDIR) + +C_SRC_DIR = $(CURDIR) +C_SRC_OUTPUT ?= $(CURDIR)/../priv/$(PROJECT).so + +# System type and C compiler/flags. + +UNAME_SYS := $(shell uname -s) +ifeq ($(UNAME_SYS), Darwin) + CC ?= cc + CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes + CXXFLAGS ?= -O3 -finline-functions -Wall + LDFLAGS ?= -flat_namespace -undefined suppress +else ifeq ($(UNAME_SYS), FreeBSD) + CC ?= cc + CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes + CXXFLAGS ?= -O3 -finline-functions -Wall +else ifeq ($(UNAME_SYS), Linux) + CC ?= gcc + CFLAGS ?= -O3 -std=c99 -finline-functions -Wall -Wmissing-prototypes + CXXFLAGS ?= -O3 -finline-functions -Wall +endif + +CFLAGS += -fPIC -I $(ERTS_INCLUDE_DIR) -I $(ERL_INTERFACE_INCLUDE_DIR) -I $(LIBTAILSCALE_LIB_DIR) +CXXFLAGS += -fPIC -I $(ERTS_INCLUDE_DIR) -I $(ERL_INTERFACE_INCLUDE_DIR) + +LDLIBS += -L $(ERL_INTERFACE_LIB_DIR) -lei -L $(LIBTAILSCALE_LIB_DIR) -ltailscale +LDFLAGS += -shared + +# Verbosity. + +c_verbose_0 = @echo " C " $(?F); +c_verbose = $(c_verbose_$(V)) + +cpp_verbose_0 = @echo " CPP " $(?F); +cpp_verbose = $(cpp_verbose_$(V)) + +link_verbose_0 = @echo " LD " $(@F); +link_verbose = $(link_verbose_$(V)) + +SOURCES := $(shell find $(C_SRC_DIR) -type f \( -name "*.c" -o -name "*.C" -o -name "*.cc" -o -name "*.cpp" \)) +OBJECTS = $(addsuffix .o, $(basename $(SOURCES))) +OBJECTS += $(LIBTAILSCALEDIR)/libtailscale.a + +COMPILE_C = $(c_verbose) $(CC) $(CFLAGS) $(CPPFLAGS) -c +COMPILE_CPP = $(cpp_verbose) $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c + +$(C_SRC_OUTPUT): $(OBJECTS) + @mkdir -p $(BASEDIR)/priv/ + $(link_verbose) $(CC) $(OBJECTS) $(LDFLAGS) $(LDLIBS) -o $(C_SRC_OUTPUT) + +$(LIBTAILSCALEDIR)/libtailscale.a: + $(MAKE) -C $(LIBTAILSCALEDIR) c-archive + +%.o: %.c + $(COMPILE_C) $(OUTPUT_OPTION) $< + +%.o: %.cc + $(COMPILE_CPP) $(OUTPUT_OPTION) $< + +%.o: %.C + $(COMPILE_CPP) $(OUTPUT_OPTION) $< + +%.o: %.cpp + $(COMPILE_CPP) $(OUTPUT_OPTION) $< + +clean: + @rm -f $(C_SRC_OUTPUT) $(OBJECTS) + $(MAKE) -C $(LIBTAILSCALEDIR) clean diff --git a/elixir/native/libtailscale_nif.c b/elixir/native/libtailscale_nif.c new file mode 100644 index 0000000..c1c5f13 --- /dev/null +++ b/elixir/native/libtailscale_nif.c @@ -0,0 +1,635 @@ +#include +#include +#include +#include +#include "tailscale.h" + +ERL_NIF_TERM atom_ok; +ERL_NIF_TERM atom_error; +ERL_NIF_TERM atom_ebadf; + +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; +} + +static ERL_NIF_TERM return_errmsg(ErlNifEnv* env, tailscale sd) { + char errmsg[512]; + /* Only read 511 characters here and insert 0 at the end. */ + if (tailscale_errmsg(sd, errmsg, 511) != 0) { + return enif_make_badarg(env); + } + errmsg[511] = '\0'; + + /* Safe because of the NULL inserted above. */ + size_t len = strlen(errmsg); + + ERL_NIF_TERM return_binary; + unsigned char* return_binary_data = enif_make_new_binary(env, len, &return_binary); + + memcpy((char * restrict)return_binary_data, errmsg, len); + + return enif_make_tuple2(env, atom_error, return_binary); +} + +static ERL_NIF_TERM tailscale_new_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + tailscale sd; + + if (argc != 0) { + return enif_make_badarg(env); + } + + sd = tailscale_new(); + return enif_make_int(env, sd); +} + +static ERL_NIF_TERM tailscale_start_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + tailscale sd; + + if (argc != 1 || !enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if (tailscale_start(sd) != 0) { + return return_errmsg(env, sd); + } else { + return atom_ok; + } +} + +static ERL_NIF_TERM tailscale_up_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + tailscale sd; + + if (argc != 1 || !enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if (tailscale_up(sd) != 0) { + return return_errmsg(env, sd); + } else { + return atom_ok; + } +} + +static ERL_NIF_TERM tailscale_close_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + tailscale sd; + + if (argc != 1 || !enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + int res = tailscale_close(sd); + if (res == EBADF) { + return enif_make_tuple2(env, atom_error, atom_ebadf); + } else if (res != 0) { + return return_errmsg(env, sd); + } else { + return atom_ok; + } +} + +static ERL_NIF_TERM tailscale_set_dir_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + tailscale sd; + ErlNifBinary bin; + + if (argc != 2) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if(!enif_inspect_binary(env, argv[1], &bin)) { + return enif_make_badarg(env); + } + + char* dir = binary_to_string(&bin); + + int ret = tailscale_set_dir(sd, dir); + free(dir); + + if (ret != 0) { + return return_errmsg(env, sd); + } else { + return atom_ok; + } +} + +static ERL_NIF_TERM tailscale_set_hostname_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + tailscale sd; + ErlNifBinary bin; + + if (argc != 2) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if(!enif_inspect_binary(env, argv[1], &bin)) { + return enif_make_badarg(env); + } + + char* hostname = binary_to_string(&bin); + + int ret = tailscale_set_hostname(sd, hostname); + + free(hostname); + + if (ret != 0) { + return return_errmsg(env, sd); + } else { + return atom_ok; + } + +} + +static ERL_NIF_TERM tailscale_set_authkey_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + int sd; + ErlNifBinary bin; + + if (argc != 2) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if(!enif_inspect_binary(env, argv[1], &bin)) { + return enif_make_badarg(env); + } + + char* authkey = binary_to_string(&bin); + + int ret = tailscale_set_authkey(sd, authkey); + + free(authkey); + + if (ret != 0) { + return return_errmsg(env, sd); + } else { + return atom_ok; + } + +} + +static ERL_NIF_TERM tailscale_set_control_url_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + int sd; + ErlNifBinary bin; + + if (argc != 2) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if(!enif_inspect_binary(env, argv[1], &bin)) { + return enif_make_badarg(env); + } + + char* control_url = binary_to_string(&bin); + + int ret = tailscale_set_control_url(sd, control_url); + + free(control_url); + + if (ret != 0) { + return return_errmsg(env, sd); + } else { + return atom_ok; + } + +} + +static ERL_NIF_TERM tailscale_set_ephemeral_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + int sd, ephemeral; + + if (argc != 2) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[1], &ephemeral)) { + return enif_make_badarg(env); + } + + if (tailscale_set_ephemeral(sd, ephemeral) != 0) { + return return_errmsg(env, sd); + } else { + return atom_ok; + } + +} + +static ERL_NIF_TERM tailscale_set_logfd_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + int sd, fd; + + if (argc != 2) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[1], &fd)) { + return enif_make_badarg(env); + } + + if (tailscale_set_logfd(sd, fd) != 0) { + return return_errmsg(env, sd); + } else { + return atom_ok; + } +} + +static ERL_NIF_TERM tailscale_getips_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + int sd; + ERL_NIF_TERM ret; + size_t len; + char* data; + char buffer[512]; + + if (argc != 1) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if (tailscale_getips(sd, buffer, 512)) { + return return_errmsg(env, sd); + } + + buffer[511] = '\0'; + len = strlen(buffer); + + data = (char*)enif_make_new_binary(env, strlen(buffer), &ret); + + memcpy(data, buffer, len); + + return enif_make_tuple2(env, atom_ok, ret); +} + +static ERL_NIF_TERM tailscale_dial_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + int sd; + ErlNifBinary network_bin; + ErlNifBinary addr_bin; + tailscale_conn conn; + + if (argc != 3) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if(!enif_inspect_binary(env, argv[1], &network_bin)) { + return enif_make_badarg(env); + } + + char* network = binary_to_string(&network_bin); + + if(!enif_inspect_binary(env, argv[2], &addr_bin)) { + return enif_make_badarg(env); + } + char* addr = binary_to_string(&addr_bin); + + int ret = tailscale_dial(sd, network, addr, &conn); + + free(addr); + free(network); + + if (ret != 0) { + return return_errmsg(env, sd); + } else { + return enif_make_tuple2(env, atom_ok, conn); + } +} + +static ERL_NIF_TERM tailscale_listen_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + int sd; + ErlNifBinary network_bin; + ErlNifBinary addr_bin; + tailscale_listener listener; + + if (argc != 3) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if(!enif_inspect_binary(env, argv[1], &network_bin)) { + return enif_make_badarg(env); + } + char* network = binary_to_string(&network_bin); + + if(!enif_inspect_binary(env, argv[2], &addr_bin)) { + return enif_make_badarg(env); + } + char* addr = binary_to_string(&addr_bin); + + int ret = tailscale_listen(sd, network, addr, &listener); + + free(addr); + free(network); + + if (ret != 0) { + return return_errmsg(env, sd); + } else { + return enif_make_tuple2(env, atom_ok, enif_make_int(env, listener)); + } +} + +static ERL_NIF_TERM tailscale_getremoteaddr_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + tailscale sd; + tailscale_listener listener; + tailscale_conn conn; + ERL_NIF_TERM ret; + size_t len; + char* data; + char buffer[512]; + + if (argc != 3) { + return enif_make_badarg(env); + } + + 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(!enif_get_int(env, argv[2], &conn)) { + return enif_make_badarg(env); + } + + if (tailscale_getremoteaddr(listener, conn, buffer, 512)) { + return return_errmsg(env, sd); + } + + buffer[511] = '\0'; + len = strlen(buffer); + + data = (char*)enif_make_new_binary(env, strlen(buffer), &ret); + + memcpy(data, buffer, len); + + return enif_make_tuple2(env, atom_ok, ret); +} + +static ERL_NIF_TERM tailscale_accept_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + tailscale sd; + tailscale_conn conn; + tailscale_listener listener; + + if (argc != 2) { + return enif_make_badarg(env); + } + + 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); + } + + + int res = tailscale_accept(listener, &conn); + if (res == EBADF) { + return enif_make_tuple2(env, atom_error, atom_ebadf); + } else if (res != 0) { + return return_errmsg(env, sd); + } else { + return enif_make_tuple2(env, atom_ok, enif_make_int(env, conn)); + } +} + +static ERL_NIF_TERM tailscale_read_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + tailscale sd; + tailscale_conn conn; + int64_t len; + ssize_t ret; + ErlNifBinary binary; + + + if(!enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[1], &conn)) { + return enif_make_badarg(env); + } + + if(!enif_get_int64(env, argv[2], &len)) { + return enif_make_badarg(env); + } + + if(!enif_alloc_binary(len, &binary)) { + return enif_make_badarg(env); + } + + ret = read(conn, binary.data, len); + if (ret > 0) { + // Read successful + if (!enif_realloc_binary(&binary, (size_t)ret)) { + return enif_make_badarg(env); + } + + return enif_make_tuple2(env, atom_ok, enif_make_binary(env, &binary)); + } else if (ret < 0) { + // Read failed + return enif_make_badarg(env); + } else { + // Nothing returned + return atom_ok; + } +} + +static ERL_NIF_TERM tailscale_write_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + tailscale sd; + tailscale_conn conn; + ssize_t ret; + ErlNifBinary binary; + + if(!enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[1], &conn)) { + return enif_make_badarg(env); + } + + if (!enif_inspect_binary(env, argv[2], &binary)) { + return enif_make_badarg(env); + } + + if ((ret = write(conn, binary.data, binary.size)) < 0) { + // An error occurred + return enif_make_badarg(env); + } + + return enif_make_tuple2(env, atom_ok, enif_make_int(env, ret)); +} + +static ERL_NIF_TERM tailscale_close_connection_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + tailscale sd; + tailscale_conn conn; + + if(!enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[1], &conn)) { + return enif_make_badarg(env); + } + + 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); + } + + return atom_ok; +} + +static ERL_NIF_TERM tailscale_loopback_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) +{ + tailscale sd; + char addr_buf[256]; + char proxy_cred[33]; + char local_api_cred[33]; + ERL_NIF_TERM addr_term, proxy_cred_term, local_api_cred_term; + size_t addr_len, proxy_cred_len, local_api_cred_len; + char* addr_data; + char* proxy_cred_data; + char* local_api_cred_data; + + if (argc != 1) { + return enif_make_badarg(env); + } + + if(!enif_get_int(env, argv[0], &sd)) { + return enif_make_badarg(env); + } + + if (tailscale_loopback(sd, addr_buf, 256, proxy_cred, local_api_cred) != 0) { + return return_errmsg(env, sd); + } + + addr_buf[255] = '\0'; + proxy_cred[32] = '\0'; + local_api_cred[32] = '\0'; + + addr_len = strlen(addr_buf); + proxy_cred_len = strlen(proxy_cred); + local_api_cred_len = strlen(local_api_cred); + + addr_data = (char*)enif_make_new_binary(env, addr_len, &addr_term); + proxy_cred_data = (char*)enif_make_new_binary(env, proxy_cred_len, &proxy_cred_term); + local_api_cred_data = (char*)enif_make_new_binary(env, local_api_cred_len, &local_api_cred_term); + + memcpy(addr_data, addr_buf, addr_len); + memcpy(proxy_cred_data, proxy_cred, proxy_cred_len); + memcpy(local_api_cred_data, local_api_cred, local_api_cred_len); + + return enif_make_tuple2(env, atom_ok, enif_make_tuple3(env, addr_term, proxy_cred_term, local_api_cred_term)); +} + +static int load(ErlNifEnv* env, void** priv, ERL_NIF_TERM load_info) +{ + atom_ok = enif_make_atom(env, "ok"); + atom_error = enif_make_atom(env, "error"); + atom_ebadf = enif_make_atom(env, "ebadf"); + + *priv = NULL; // No module-level private data needed for this example + + return 0; +} + +// Unload callback: Called when the NIF library is unloaded (e.g., code purge) +static void unload(ErlNifEnv* env, void* priv_data) { + // Perform any necessary cleanup of module-level private data if it existed + fprintf(stderr, "NIF: Library unloaded\n"); +} + +static ErlNifFunc nif_funcs[] = { + {"new", 0, tailscale_new_nif}, + {"start", 1, tailscale_start_nif}, + {"up", 1, tailscale_up_nif}, + {"close", 1, tailscale_close_nif}, + {"set_dir", 2, tailscale_set_dir_nif}, + {"set_hostname", 2, tailscale_set_hostname_nif}, + {"set_authkey", 2, tailscale_set_authkey_nif}, + {"set_control_url", 2, tailscale_set_control_url_nif}, + {"set_ephemeral", 2, tailscale_set_ephemeral_nif}, + {"set_logfd", 2, tailscale_set_logfd_nif}, + {"getips", 1, tailscale_getips_nif}, + {"dial", 3, tailscale_dial_nif}, + {"listen", 3, tailscale_listen_nif}, + {"getremoteaddr", 3, tailscale_getremoteaddr_nif}, + {"accept", 2, tailscale_accept_nif}, + {"read", 3, tailscale_read_nif}, + {"write", 3, tailscale_write_nif}, + {"close_connection", 2, tailscale_close_connection_nif}, + {"close_listener", 2, tailscale_close_listener_nif}, + {"loopback", 1, tailscale_loopback_nif}, +}; + +ERL_NIF_INIT(Elixir.Libtailscale, nif_funcs, &load, NULL, NULL, unload) diff --git a/elixir/priv/.gitignore b/elixir/priv/.gitignore new file mode 100644 index 0000000..7b5aafe --- /dev/null +++ b/elixir/priv/.gitignore @@ -0,0 +1,2 @@ +# Don't commit copied files from parent directory +/libtailscale