mirror of
https://github.com/Drop-OSS/libtailscale.git
synced 2026-01-30 20:55:18 +01:00
Use a socketpair(2) and sendmsg/recvmsg to pass a connection fd from Go to C. This lets people write non-blocking C by polling on a tailscale_listener for when they should tailscale_accept. Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
285 lines
9.5 KiB
Ruby
285 lines
9.5 KiB
Ruby
# 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 :close, [:int], :int
|
|
attach_function :tailscale_accept, [:int, :pointer], :int, blocking: true
|
|
attach_function :TsnetErrmsg, [:int, :pointer, :size_t], :int
|
|
attach_function :TsnetLoopback, [:int, :pointer, :size_t, :pointer, :pointer], :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::close(@listener)
|
|
end
|
|
end
|
|
|
|
# LocalAPIClient provides a Net::HTTP-alike API that can be used to make
|
|
# authenticated requests to the local tailscale API. For higher level use,
|
|
# +LocalAPI+ may be more convenient.
|
|
class LocalAPIClient
|
|
# address is the host:port address of the nodes LocalAPI server.
|
|
attr_reader :address
|
|
# credential is the basic-auth password used to authenticate requests.
|
|
attr_reader :credential
|
|
|
|
def initialize(addr, cred)
|
|
@address = addr
|
|
@credential = cred
|
|
@basic = Base64.strict_encode64(":#{cred}")
|
|
host, _, port = addr.rpartition(":")
|
|
@http = Net::HTTP.new(host, port)
|
|
end
|
|
|
|
def head(path, initheader = nil, &block)
|
|
request Net::HTTP::Head.new(path, initheader), &block
|
|
end
|
|
|
|
def get(path, initheader = nil, &block)
|
|
request Net::HTTP::Get.new(path, initheader), &block
|
|
end
|
|
|
|
def post(path, body = nil, initheader = nil, &block)
|
|
request Net::HTTP::Post.new(path, initheader), body, &block
|
|
end
|
|
|
|
def put(path, body = nil, initheader = nil, &block)
|
|
request Net::HTTP::Put.new(path, initheader), body, &block
|
|
end
|
|
|
|
def patch(path, body = nil, initheader = nil, &block)
|
|
request Net::HTTP::Patch.new(path, initheader), body, &block
|
|
end
|
|
|
|
def delete(path, initheader = nil, &block)
|
|
request Net::HTTP::Delete.new(path, initheader), &block
|
|
end
|
|
|
|
def request(req, body = nil, &block)
|
|
req["Host"] = @address
|
|
req["Authorization"] = "Basic #{@basic}"
|
|
req["Sec-Tailscale"] = "localapi"
|
|
@http.request(req, body, &block)
|
|
end
|
|
end
|
|
|
|
# LocalAPI provides a convenient interface for interacting with a LocalAPI given a
|
|
# LocalAPIClient to make requests with.
|
|
class LocalAPI
|
|
|
|
def initialize(client)
|
|
@client = client
|
|
end
|
|
|
|
# status returns the status of the local tailscale node.
|
|
def status
|
|
@client.get("/localapi/v0/status") do |r|
|
|
return JSON.parse(r.body)
|
|
end
|
|
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
|
|
|
|
# Start a listener on a loopback address, and returns the address
|
|
# and credentials for using it as LocalAPI or a proxy.
|
|
def loopback
|
|
assert_open
|
|
addrbuf = FFI::MemoryPointer.new(:char, 1024)
|
|
proxycredbuf = FFI::MemoryPointer.new(:char, 33)
|
|
localcredbuf = FFI::MemoryPointer.new(:char, 33)
|
|
Error.check self, Libtailscale::TsnetLoopback(@t, addrbuf, addrbuf.size, proxycredbuf, localcredbuf)
|
|
[addrbuf.read_string, proxycredbuf.read_string, localcredbuf.read_string]
|
|
end
|
|
|
|
# Start the local API and return a LocalAPIClient for interacting with it.
|
|
def local_api_client
|
|
addr, _, cred = loopback
|
|
LocalAPIClient.new(addr, cred)
|
|
end
|
|
|
|
# Start the local API and return a LocalAPI for interacting with it.
|
|
def local_api
|
|
LocalAPI.new(local_api_client)
|
|
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
|