mirror of
https://github.com/Drop-OSS/libtailscale.git
synced 2026-01-30 20:55:18 +01:00
ruby: add a gem that wraps the library
This commit is contained in:
36
.github/workflows/ruby.yml
vendored
Normal file
36
.github/workflows/ruby.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
name: Ruby
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Ruby ${{ matrix.ruby }}
|
||||
strategy:
|
||||
matrix:
|
||||
ruby:
|
||||
- '3.2.0'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "1.20"
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: ${{ matrix.ruby }}
|
||||
bundler-cache: true
|
||||
working-directory: ruby
|
||||
- name: Run build & test
|
||||
run: |
|
||||
cd ruby
|
||||
bundle exec rake build test
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,2 +1,9 @@
|
||||
libtailscale.so
|
||||
libtailscale.a
|
||||
libtailscale.h
|
||||
/ruby/tmp/
|
||||
/ruby/doc/
|
||||
/ruby/ext/libtailscale/*.go
|
||||
/ruby/ext/libtailscale/go.mod
|
||||
/ruby/ext/libtailscale/go.sum
|
||||
/ruby/LICENSE
|
||||
12
ruby/Gemfile
Normal file
12
ruby/Gemfile
Normal file
@@ -0,0 +1,12 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
# Specify your gem's dependencies in tailscale-ruby.gemspec
|
||||
gemspec
|
||||
|
||||
gem "rake", "~> 13.0"
|
||||
gem "rake-compiler", "~> 1.2.1"
|
||||
gem "minitest", "~> 5.0"
|
||||
26
ruby/Gemfile.lock
Normal file
26
ruby/Gemfile.lock
Normal file
@@ -0,0 +1,26 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
tailscale (0.1.0)
|
||||
ffi (~> 1.15.5)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
ffi (1.15.5)
|
||||
minitest (5.16.3)
|
||||
rake (13.0.6)
|
||||
rake-compiler (1.2.1)
|
||||
rake
|
||||
|
||||
PLATFORMS
|
||||
x86_64-linux-gnu
|
||||
|
||||
DEPENDENCIES
|
||||
minitest (~> 5.0)
|
||||
rake (~> 13.0)
|
||||
rake-compiler (~> 1.2.1)
|
||||
tailscale!
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.1
|
||||
47
ruby/README.md
Normal file
47
ruby/README.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# tailscale
|
||||
|
||||
The tailscale gem provides an embedded network interface that can be used to
|
||||
listen for and dial connections to other [Tailscale](https://tailscale.com)
|
||||
nodes.
|
||||
|
||||
## Installation
|
||||
|
||||
Source installations will require a recent Go compiler in $PATH in order to build.
|
||||
|
||||
Install the gem and add to the application's Gemfile by executing:
|
||||
|
||||
$ bundle add tailscale
|
||||
|
||||
If bundler is not being used to manage dependencies, install the gem by executing:
|
||||
|
||||
$ gem install ailscale
|
||||
|
||||
## Usage
|
||||
|
||||
The node will need to be authorized in order to function. Set an auth key with
|
||||
`set_auth_key`, or watch the libtailscale log stream and respond to the printed
|
||||
authorization URL. You can also set the `$TS_AUTHKEY` environment variable.
|
||||
|
||||
```ruby
|
||||
require 'tailscale'
|
||||
t = Tailscale.new
|
||||
t.up
|
||||
l = t.listen "tcp", ":1999"
|
||||
while c = l.accept
|
||||
c.write "hello world"
|
||||
c.close
|
||||
end
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
||||
|
||||
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are welcome on GitHub at https://github.com/tailscale/libtailscale
|
||||
|
||||
Please file any issues about this code or the hosted service on
|
||||
[the issue tracker](https://github.com/tailscale/tailscale/issues).
|
||||
53
ruby/Rakefile
Normal file
53
ruby/Rakefile
Normal file
@@ -0,0 +1,53 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "bundler/gem_tasks"
|
||||
require "rake/testtask"
|
||||
require "rake/extensiontask"
|
||||
|
||||
go_sources = %w[tailscale.go go.mod go.sum]
|
||||
go_sources.map do |f|
|
||||
to = "ext/libtailscale/#{f}"
|
||||
from = "../#{f}"
|
||||
file to => from do
|
||||
cp from, to
|
||||
end
|
||||
task copy: to
|
||||
task :clobber do
|
||||
rm_f to
|
||||
end
|
||||
end
|
||||
file "LICENSE" => "../LICENSE" do
|
||||
cp "../LICENSE", "LICENSE"
|
||||
end
|
||||
task :clobber do
|
||||
rm_f "LICENSE"
|
||||
end
|
||||
task copy: "LICENSE"
|
||||
task build: :copy
|
||||
|
||||
# XXX: Rake::ExtensionTask seems to ignore prerequisites.
|
||||
# Rake::ExtensionTask.new "libtailscale" do |ext|
|
||||
# ext.source_pattern = "*.{go,mod,sum}"
|
||||
# end
|
||||
# task "compile:libtailscale" => :copy
|
||||
libname = "lib/libtailscale.#{RbConfig::CONFIG['DLEXT']}"
|
||||
task libname => :copy do |t|
|
||||
sh "go build -buildmode=c-shared -o #{t.name} github.com/tailscale/libtailscale"
|
||||
end
|
||||
desc "Build the C extension using local sources"
|
||||
task compile: libname
|
||||
task :clobber do
|
||||
rm_f libname
|
||||
rm_f libname.sub(/\.#{RbConfig::CONFIG['DLEXT']}$/, ".h")
|
||||
end
|
||||
|
||||
Rake::TestTask.new(:test) do |t|
|
||||
t.libs << "test"
|
||||
t.libs << "lib"
|
||||
t.test_files = FileList["test/**/test_*.rb"]
|
||||
end
|
||||
task test: :compile
|
||||
|
||||
task default: :test
|
||||
19
ruby/bin/echo_server
Executable file
19
ruby/bin/echo_server
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env ruby
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "bundler/setup"
|
||||
require "tailscale"
|
||||
|
||||
t = Tailscale::new
|
||||
t.start
|
||||
|
||||
s = t.listen "tcp", ":1997"
|
||||
while c = s.accept
|
||||
while got = c.readpartial(2046)
|
||||
print got
|
||||
c.write got
|
||||
end
|
||||
c.close
|
||||
end
|
||||
12
ruby/ext/libtailscale/extconf.rb
Normal file
12
ruby/ext/libtailscale/extconf.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# frozen_string_literal: true
|
||||
require 'rbconfig'
|
||||
open("Makefile", "w") do |f|
|
||||
f.puts "libtailscale.#{RbConfig::CONFIG['DLEXT']}:"
|
||||
f.puts "\tgo build -C #{File.expand_path(__dir__)} -buildmode=c-shared -o #{Dir.pwd}/$@ ."
|
||||
|
||||
f.puts "install: libtailscale.#{RbConfig::CONFIG['DLEXT']}"
|
||||
f.puts "\tmkdir -p #{RbConfig::CONFIG['sitelibdir']}"
|
||||
f.puts "\tcp libtailscale.#{RbConfig::CONFIG['DLEXT']} #{RbConfig::CONFIG['sitelibdir']}/"
|
||||
end
|
||||
195
ruby/lib/tailscale.rb
Normal file
195
ruby/lib/tailscale.rb
Normal file
@@ -0,0 +1,195 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'tailscale/version'
|
||||
require 'ffi'
|
||||
require 'rbconfig'
|
||||
|
||||
# Tailscale provides an embedded tailscale network interface for ruby programs.
|
||||
class Tailscale
|
||||
|
||||
# Libtailscale is a FFI wrapper around the libtailscale C library.
|
||||
module Libtailscale
|
||||
extend FFI::Library
|
||||
|
||||
# In development or in precompiled gems the library is in the lib
|
||||
# directory, and when installed by rubygems it's in the ruby site lib
|
||||
# directory.
|
||||
[__dir__, RbConfig::CONFIG['sitelibdir']].find do |dir|
|
||||
lib = File.expand_path("libtailscale.#{RbConfig::CONFIG["DLEXT"]}", dir)
|
||||
if File.exist?(lib)
|
||||
ffi_lib lib
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
attach_function :TsnetNewServer, [], :int
|
||||
attach_function :TsnetStart, [:int], :int
|
||||
attach_function :TsnetUp, [:int], :int, blocking: true
|
||||
attach_function :TsnetClose, [:int], :int
|
||||
attach_function :TsnetSetDir, [:int, :string], :int
|
||||
attach_function :TsnetSetHostname, [:int, :string], :int
|
||||
attach_function :TsnetSetAuthKey, [:int, :string], :int
|
||||
attach_function :TsnetSetControlURL, [:int, :string], :int
|
||||
attach_function :TsnetSetEphemeral, [:int, :int], :int
|
||||
attach_function :TsnetSetLogFD, [:int, :int], :int
|
||||
attach_function :TsnetDial, [:int, :string, :string, :pointer], :int, blocking: true
|
||||
attach_function :TsnetListen, [:int, :string, :string, :pointer], :int
|
||||
attach_function :TsnetListenerClose, [:int], :int
|
||||
attach_function :TsnetAccept, [:int, :pointer], :int, blocking: true
|
||||
attach_function :TsnetErrmsg, [:int, :pointer, :size_t], :int
|
||||
end
|
||||
|
||||
class ClosedError < StandardError
|
||||
def initialize
|
||||
super "tailscale error: the server is closed"
|
||||
end
|
||||
end
|
||||
|
||||
class Error < StandardError
|
||||
attr_reader :code
|
||||
|
||||
def initialize(msg, code = -1)
|
||||
@code = code
|
||||
super msg
|
||||
end
|
||||
|
||||
def self.check(ts, code)
|
||||
return if code == 0
|
||||
|
||||
if code == -1
|
||||
msg = ts.errmsg
|
||||
else
|
||||
msg = "tailscale error: code: #{code}"
|
||||
end
|
||||
raise Error.new(msg, code)
|
||||
end
|
||||
end
|
||||
|
||||
# A listening socket on the tailscale network.
|
||||
class Listener
|
||||
# Create a new listener, user code should not call this directly,
|
||||
# instead use +Tailscale#listen+.
|
||||
def initialize(ts, listener)
|
||||
@ts = ts
|
||||
@listener = listener
|
||||
end
|
||||
|
||||
# Accept a new connection. This method blocks until a new connection is
|
||||
# recieved. An +IO+ object is returned which can be used to read and
|
||||
# write.
|
||||
def accept
|
||||
@ts.assert_open
|
||||
conn = FFI::MemoryPointer.new(:int)
|
||||
Error.check @ts, Libtailscale::TsnetAccept(@listener, conn)
|
||||
IO::new conn.read_int
|
||||
end
|
||||
|
||||
# Close the listener.
|
||||
def close
|
||||
@ts.assert_open
|
||||
Error.check @ts, Libtailscale::TsnetListenerClose(@listener)
|
||||
end
|
||||
end
|
||||
|
||||
# Create a new tailscale server.
|
||||
#
|
||||
# The server is not started, and no network traffic will occur until start
|
||||
# is called or network operations are used (such as dial or listen).
|
||||
def initialize
|
||||
@t = Libtailscale::TsnetNewServer()
|
||||
raise Error.new("tailscale error: failed to initialize", @t) if @t < 0
|
||||
end
|
||||
|
||||
# Start the tailscale server asynchronously.
|
||||
def start
|
||||
Error.check self, Libtailscale::TsnetStart(@t)
|
||||
end
|
||||
|
||||
# Bring the tailscale server up and wait for it to be usable. This method
|
||||
# blocks until the node is fully authorized.
|
||||
def up
|
||||
Error.check self, Libtailscale::TsnetUp(@t)
|
||||
end
|
||||
|
||||
# Close the tailscale server.
|
||||
def close
|
||||
Error.check self, Libtailscale::TsnetClose(@t)
|
||||
@t = -1
|
||||
end
|
||||
|
||||
# Set the directory to store tailscale state in.
|
||||
def set_dir(dir)
|
||||
assert_open
|
||||
Error.check self, Libtailscale::TsnetSetDir(@t, dir)
|
||||
end
|
||||
|
||||
# Set the hostname to use for the tailscale node.
|
||||
def set_hostname(hostname)
|
||||
assert_open
|
||||
Error.check self, Libtailscale::TsnetSetHostname(@t, hostname)
|
||||
end
|
||||
|
||||
# Set the auth key to use for the tailscale node.
|
||||
def set_auth_key(auth_key)
|
||||
assert_open
|
||||
Error.check self, Libtailscale::TsnetSetAuthKey(@t, auth_key)
|
||||
end
|
||||
|
||||
# Set the control URL the node will connect to.
|
||||
def set_control_url(control_url)
|
||||
assert_open
|
||||
Error.check self, Libtailscale::TsnetSetControlURL(@t, control_url)
|
||||
end
|
||||
|
||||
# Set whether the node is ephemeral or not.
|
||||
def set_ephemeral(ephemeral)
|
||||
assert_open
|
||||
Error.check self, Libtailscale::TsnetSetEphemeral(@t, ephemeral ? 1 : 0)
|
||||
end
|
||||
|
||||
# Set the file descriptor to use for logging. The file descriptor must be
|
||||
# open for writing. e.g. use `IO.sysopen("/dev/null", "w")` to disable
|
||||
# logging.
|
||||
def set_log_fd(log_fd)
|
||||
assert_open
|
||||
Error.check self, Libtailscale::TsnetSetLogFD(@t, log_fd)
|
||||
end
|
||||
|
||||
# Dial a network address. +network+ is one of "tcp" or "udp". +addr+ is the
|
||||
# remote address to connect to, and +local_addr+ is the local address to
|
||||
# bind to. This method blocks until the connection is established.
|
||||
def dial(network, addr, local_addr)
|
||||
assert_open
|
||||
conn = FFI::MemoryPointer.new(:int)
|
||||
Error.check self, Libtailscale::TsnetDial(@t, network, addr, conn)
|
||||
IO::new conn.read_int
|
||||
end
|
||||
|
||||
# Listen on a network address. +network+ is one of "tcp" or "udp". +addr+ is
|
||||
# the local address to bind to.
|
||||
def listen(network, addr)
|
||||
assert_open
|
||||
listener = FFI::MemoryPointer.new(:int)
|
||||
Error.check self, Libtailscale::TsnetListen(@t, network, addr, listener)
|
||||
Listener.new self, listener.read_int
|
||||
end
|
||||
|
||||
# Get the last detailed error message from the tailscale server. This method
|
||||
# is typically not needed by user code, as the library will raise an
|
||||
# +Error+ with the error message.
|
||||
def errmsg
|
||||
buf = FFI::MemoryPointer.new(:char, 1024)
|
||||
r = Libtailscale::TsnetErrmsg(@t, buf, buf.size)
|
||||
if r != 0
|
||||
return "tailscale internal error: failed to get error message"
|
||||
end
|
||||
buf.read_string
|
||||
end
|
||||
|
||||
# Check if the tailscale server is open.
|
||||
def assert_open
|
||||
raise ClosedError if @t <= 0
|
||||
end
|
||||
end
|
||||
8
ruby/lib/tailscale/version.rb
Normal file
8
ruby/lib/tailscale/version.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Tailscale
|
||||
VERSION = "0.1.0"
|
||||
end
|
||||
39
ruby/tailscale.gemspec
Normal file
39
ruby/tailscale.gemspec
Normal file
@@ -0,0 +1,39 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "lib/tailscale/version"
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "tailscale"
|
||||
spec.version = Tailscale::VERSION
|
||||
spec.authors = ["Tailscale Inc & AUTHORS"]
|
||||
spec.email = ["support@tailscale.com"]
|
||||
|
||||
spec.summary = "Tailscale in-process connections for Ruby"
|
||||
spec.description = "Tailscale in-process connections for Ruby"
|
||||
spec.homepage = "https://www.tailscale.com"
|
||||
spec.license = "BSD-3-Clause"
|
||||
spec.required_ruby_version = ">= 2.6.0"
|
||||
|
||||
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
||||
|
||||
spec.metadata["homepage_uri"] = spec.homepage
|
||||
spec.metadata["source_code_uri"] = "https://github.com/tailscale/libtailscale/tree/main/ruby"
|
||||
spec.metadata["bug_tracker_uri"] = "https://github.com/tailscale/tailscale/issues"
|
||||
|
||||
spec.files = Dir.chdir(__dir__) do
|
||||
`git ls-files -z`.split("\x0").reject do |f|
|
||||
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
|
||||
end
|
||||
end
|
||||
spec.files += ["LICENSE"]
|
||||
spec.files += Dir["ext/libtailscale/*.{mod,sum,go}"]
|
||||
|
||||
spec.bindir = "exe"
|
||||
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
||||
spec.require_paths = ["lib"]
|
||||
spec.extensions = ["ext/libtailscale/extconf.rb"]
|
||||
|
||||
spec.add_dependency "ffi", "~> 1.15.5"
|
||||
end
|
||||
38
ruby/test/tailscale/test_tailscale.rb
Normal file
38
ruby/test/tailscale/test_tailscale.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# frozen_string_literal: true
|
||||
require 'test_helper'
|
||||
|
||||
class TestTailscale < Minitest::Test
|
||||
|
||||
def test_that_it_has_a_version_number
|
||||
refute_nil ::Tailscale::VERSION
|
||||
end
|
||||
|
||||
def test_listen_sorta_works
|
||||
# TODO: make a more useful test when we can make a client to connect with.
|
||||
ts = newts
|
||||
ts.start
|
||||
s = ts.listen "tcp", ":1999"
|
||||
s.close
|
||||
ts.close
|
||||
end
|
||||
|
||||
def test_dial_sorta_works
|
||||
# TODO: make a more useful test when we can make a server to connect to.
|
||||
ts = newts
|
||||
ts.start
|
||||
c = ts.dial "udp", "100.100.100.100:53", ""
|
||||
c.close
|
||||
ts.close
|
||||
end
|
||||
|
||||
def newts
|
||||
t = Tailscale::new
|
||||
unless ENV['VERBOSE']
|
||||
logfd = IO.sysopen("/dev/null", "w+")
|
||||
t.set_log_fd logfd
|
||||
end
|
||||
t
|
||||
end
|
||||
end
|
||||
6
ruby/test/test_helper.rb
Normal file
6
ruby/test/test_helper.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "tailscale"
|
||||
require "minitest/autorun"
|
||||
Reference in New Issue
Block a user