diff --git a/ruby/Gemfile.lock b/ruby/Gemfile.lock index 76d7c54..6feaa55 100644 --- a/ruby/Gemfile.lock +++ b/ruby/Gemfile.lock @@ -14,6 +14,7 @@ GEM rake PLATFORMS + arm64-darwin-22 x86_64-linux-gnu DEPENDENCIES diff --git a/ruby/lib/tailscale.rb b/ruby/lib/tailscale.rb index c9f9fce..06caad4 100644 --- a/ruby/lib/tailscale.rb +++ b/ruby/lib/tailscale.rb @@ -5,6 +5,9 @@ require 'tailscale/version' require 'ffi' require 'rbconfig' +require 'base64' +require 'net/http' +require 'json' # Tailscale provides an embedded tailscale network interface for ruby programs. class Tailscale @@ -36,8 +39,7 @@ module Libtailscale 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 :TsnetAccept, [:int, :pointer], :int, blocking: true attach_function :TsnetErrmsg, [:int, :pointer, :size_t], :int attach_function :TsnetLoopback, [:int, :pointer, :size_t, :pointer, :pointer], :int end @@ -82,6 +84,7 @@ def initialize(ts, listener) # write. def accept @ts.assert_open + IO.select [IO.for_fd(@listener)] conn = FFI::MemoryPointer.new(:int) Error.check @ts, Libtailscale::TsnetAccept(@listener, conn) IO::new conn.read_int @@ -90,7 +93,7 @@ def accept # Close the listener. def close @ts.assert_open - Error.check @ts, Libtailscale::close(@listener) + IO.for_fd(@listener).close end end @@ -225,9 +228,8 @@ def set_log_fd(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) + # remote address to connect to. This method blocks until the connection is established. + def dial(network, addr) assert_open conn = FFI::MemoryPointer.new(:int) Error.check self, Libtailscale::TsnetDial(@t, network, addr, conn) diff --git a/ruby/test/tailscale/test_tailscale.rb b/ruby/test/tailscale/test_tailscale.rb index 8e1b8a0..7f2aa35 100644 --- a/ruby/test/tailscale/test_tailscale.rb +++ b/ruby/test/tailscale/test_tailscale.rb @@ -22,11 +22,28 @@ 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 = ts.dial "udp", "100.100.100.100:53" c.close ts.close end + # Requires a solution to be logged in: + # def test_listen_accept_dial_close + # ts = newts + # ts.up + # hn = ts.local_api.status["Self"]["HostName"] + # s = ts.listen "tcp", ":1999" + # c = ts.dial "tcp", "#{hn}:1999" + # ss = s.accept + # c.write "hello" + # assert_equal "hello", ss.read(5) + # ss.write "world" + # assert_equal "world", c.read(5) + # ss.close + # c.close + # ts.close + # end + def newts t = Tailscale::new unless ENV['VERBOSE'] diff --git a/tailscale.c b/tailscale.c index 7a39f50..bf05902 100644 --- a/tailscale.c +++ b/tailscale.c @@ -20,6 +20,7 @@ extern int TsnetSetControlURL(int sd, char* str); extern int TsnetSetEphemeral(int sd, int ephemeral); extern int TsnetSetLogFD(int sd, int fd); extern int TsnetListen(int sd, char* net, char* addr, int* listenerOut); +extern int TsnetAccept(int ld, int* connOut); extern int TsnetLoopback(int sd, char* addrOut, size_t addrLen, char* proxyOut, char* localOut); tailscale tailscale_new() { @@ -47,27 +48,7 @@ int tailscale_listen(tailscale sd, const char* network, const char* addr, tailsc } int tailscale_accept(tailscale_listener ld, tailscale_conn* conn_out) { - struct msghdr msg = {0}; - - char mbuf[256]; - struct iovec io = { .iov_base = mbuf, .iov_len = sizeof(mbuf) }; - msg.msg_iov = &io; - msg.msg_iovlen = 1; - - char cbuf[256]; - msg.msg_control = cbuf; - msg.msg_controllen = sizeof(cbuf); - - if (recvmsg(ld, &msg, 0) == -1) { - return -1; - } - - struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg); - unsigned char* data = CMSG_DATA(cmsg); - - int fd = *(int*)data; - *conn_out = fd; - return 0; + return TsnetAccept(ld, (int*)conn_out); } int tailscale_set_dir(tailscale sd, const char* dir) { diff --git a/tailscale.go b/tailscale.go index 7087ed0..dc0737a 100644 --- a/tailscale.go +++ b/tailscale.go @@ -17,6 +17,7 @@ import ( "syscall" "unsafe" + "golang.org/x/sys/unix" "tailscale.com/hostinfo" "tailscale.com/tsnet" "tailscale.com/types/logger" @@ -254,6 +255,41 @@ func TsnetListen(sd C.int, network, addr *C.char, listenerOut *C.int) C.int { return 0 } +//export TsnetAccept +func TsnetAccept(listenerFd C.int, connOut *C.int) C.int { + listeners.mu.Lock() + ln := listeners.m[listenerFd] + listeners.mu.Unlock() + + if ln == nil { + return C.EBADF + } + + buf := make([]byte, unix.CmsgLen(int(unsafe.Sizeof((C.int)(0))))) + _, oobn, _, _, err := syscall.Recvmsg(int(listenerFd), nil, buf, 0) + if err != nil { + return ln.s.recErr(err) + } + + scms, err := syscall.ParseSocketControlMessage(buf[:oobn]) + if err != nil { + return ln.s.recErr(err) + } + if len(scms) != 1 { + return ln.s.recErr(fmt.Errorf("libtailscale: got %d control messages, want 1", len(scms))) + } + fds, err := syscall.ParseUnixRights(&scms[0]) + if err != nil { + return ln.s.recErr(err) + } + if len(fds) != 1 { + return ln.s.recErr(fmt.Errorf("libtailscale: got %d FDs, want 1", len(fds))) + } + *connOut = (C.int)(fds[0]) + + return 0 +} + func newConn(s *server, netConn net.Conn, connOut *C.int) error { fds, err := syscall.Socketpair(syscall.AF_LOCAL, syscall.SOCK_STREAM, 0) if err != nil {