mirror of
https://github.com/Drop-OSS/libtailscale-drop.git
synced 2026-02-04 06:51:18 +01:00
python: add initial support for Python
Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
This commit is contained in:
12
python/.gitignore
vendored
Normal file
12
python/.gitignore
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
.venv/
|
||||
|
||||
__pycache__/
|
||||
tailscale.egg-info/
|
||||
dist/
|
||||
build/
|
||||
*.whl
|
||||
|
||||
pybind11/
|
||||
|
||||
libtailscale.a
|
||||
libtailscale.h
|
||||
17
python/CMakeLists.txt
Normal file
17
python/CMakeLists.txt
Normal file
@@ -0,0 +1,17 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
cmake_minimum_required(VERSION 3.4...3.18)
|
||||
project(tailscale)
|
||||
|
||||
add_subdirectory(pybind11)
|
||||
|
||||
link_directories(.)
|
||||
|
||||
pybind11_add_module(_tailscale src/main.cpp)
|
||||
|
||||
target_link_libraries(_tailscale PRIVATE tailscale)
|
||||
|
||||
target_compile_definitions(_tailscale PRIVATE VERSION_INFO=${TAILSCALE_VERSION_INFO})
|
||||
|
||||
target_include_directories(_tailscale PRIVATE .)
|
||||
16
python/Makefile
Normal file
16
python/Makefile
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
build:
|
||||
@git clone https://github.com/pybind/pybind11 || true
|
||||
cd pybind11 && git checkout 3cc7e4258c15a6a19ba5e0b62a220b1a6196d4eb
|
||||
cd .. && go build -buildmode=c-archive -o python/libtailscale.a github.com/tailscale/libtailscale
|
||||
pip install .
|
||||
|
||||
wheel:
|
||||
pip wheel .
|
||||
|
||||
clean:
|
||||
rm -rf pybind11/ libtailscale.a libtailscale.h dist/ build/ tailscale.egg-info/
|
||||
|
||||
.PHONY: build wheel clean
|
||||
43
python/README.md
Normal file
43
python/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# tailscale
|
||||
|
||||
The tailscale Python package provides an embedded network interface that can be
|
||||
used to listen for and dial connections to other [Tailscale](https://tailscale.com) nodes.
|
||||
|
||||
## Build and Install
|
||||
|
||||
Build Requirements:
|
||||
- Python 3.9 or greater
|
||||
- A recent Go compiler in $PATH
|
||||
- CMake (and a C compiler)
|
||||
- Git
|
||||
|
||||
Start by creating a virtualenv:
|
||||
|
||||
$ python3 -m venv venv
|
||||
$ source venv/bin/activate
|
||||
|
||||
Install build dependencies, build the c-archive, and install the Python package in your virtualenv:
|
||||
|
||||
$ make build
|
||||
|
||||
Run example echo server:
|
||||
|
||||
$ python3 examples/echo.py
|
||||
|
||||
Build a distributable wheel:
|
||||
|
||||
$ make wheel
|
||||
=> tailscale-0.0.1-cp310-cp310-linux_x86_64.whl
|
||||
|
||||
## Usage
|
||||
|
||||
The node will need to be authorized in order to function. Set an auth key via
|
||||
the `$TS_AUTHKEY` environment variable, with `TSNet.set_authkey`, or watch the log
|
||||
stream and respond to the printed authorization URL.
|
||||
|
||||
## 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).
|
||||
42
python/examples/echo.py
Normal file
42
python/examples/echo.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
# TODO(shayne): proper select/poll/epoll + os.set_blocking(conn, False)
|
||||
import os
|
||||
import select
|
||||
from tailscale import TSNet
|
||||
|
||||
def handler(conn):
|
||||
while True:
|
||||
r, _, _ = select.select([conn], [], [], 10)
|
||||
if not conn in r:
|
||||
os._exit(0)
|
||||
data = os.read(conn, 2048)
|
||||
print(data.decode(), end="")
|
||||
|
||||
|
||||
def main():
|
||||
procs = []
|
||||
|
||||
ts = TSNet(ephemeral=True)
|
||||
ts.up()
|
||||
|
||||
ln = ts.listen("tcp", ":1999")
|
||||
while True:
|
||||
while procs:
|
||||
pid, exit_code = os.waitpid(-1, os.WNOHANG)
|
||||
if pid == 0:
|
||||
break
|
||||
procs.remove(pid)
|
||||
|
||||
conn = ln.accept()
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
return handler(conn)
|
||||
procs.append(pid)
|
||||
|
||||
ln.close()
|
||||
ts.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
42
python/flake.lock
generated
Normal file
42
python/flake.lock
generated
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1676283394,
|
||||
"narHash": "sha256-XX2f9c3iySLCw54rJ/CZs+ZK6IQy7GXNY4nSOyu2QG4=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "3db36a8b464d0c4532ba1c7dda728f4576d6d073",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1677932085,
|
||||
"narHash": "sha256-+AB4dYllWig8iO6vAiGGYl0NEgmMgGHpy9gzWJ3322g=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3c5319ad3aa51551182ac82ea17ab1c6b0f0df89",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-unstable",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
27
python/flake.nix
Normal file
27
python/flake.nix
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
devEnv = (pkgs.buildFHSUserEnv {
|
||||
name = "libtailscale-python";
|
||||
targetPkgs = pkgs: (with pkgs; [
|
||||
cmake
|
||||
python39
|
||||
]);
|
||||
runScript = "${pkgs.writeShellScriptBin "runScript" (''
|
||||
set -e
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
exec bash
|
||||
'')}/bin/runScript";
|
||||
}).env;
|
||||
in {
|
||||
devShell = devEnv;
|
||||
}
|
||||
);
|
||||
}
|
||||
49
python/pyproject.toml
Normal file
49
python/pyproject.toml
Normal file
@@ -0,0 +1,49 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=42",
|
||||
"wheel",
|
||||
"cmake>=3.12",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.mypy]
|
||||
files = "setup.py"
|
||||
python_version = "3.7"
|
||||
strict = true
|
||||
show_error_codes = true
|
||||
enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
|
||||
warn_unreachable = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
ignore_missing_imports = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
addopts = ["-ra", "--showlocals", "--strict-markers", "--strict-config"]
|
||||
xfail_strict = true
|
||||
filterwarnings = ["error"]
|
||||
testpaths = ["tests"]
|
||||
|
||||
[tool.cibuildwheel]
|
||||
test-command = "pytest {project}/tests"
|
||||
test-extras = ["test"]
|
||||
test-skip = ["*universal2:arm64"]
|
||||
# Setuptools bug causes collision between pypy and cpython artifacts
|
||||
before-build = "rm -rf {project}/build"
|
||||
|
||||
[tool.ruff]
|
||||
extend-select = [
|
||||
"B", # flake8-bugbear
|
||||
"B904",
|
||||
"I", # isort
|
||||
"PGH", # pygrep-hooks
|
||||
"RUF", # Ruff-specific
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
extend-ignore = [
|
||||
"E501", # Line too long
|
||||
]
|
||||
target-version = "py37"
|
||||
141
python/setup.py
Normal file
141
python/setup.py
Normal file
@@ -0,0 +1,141 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from setuptools import Extension, setup
|
||||
from setuptools.command.build_ext import build_ext
|
||||
|
||||
# Convert distutils Windows platform specifiers to CMake -A arguments
|
||||
PLAT_TO_CMAKE = {
|
||||
"win32": "Win32",
|
||||
"win-amd64": "x64",
|
||||
"win-arm32": "ARM",
|
||||
"win-arm64": "ARM64",
|
||||
}
|
||||
|
||||
# A CMakeExtension needs a sourcedir instead of a file list.
|
||||
# The name must be the _single_ output extension from the CMake build.
|
||||
# If you need multiple extensions, see scikit-build.
|
||||
class CMakeExtension(Extension):
|
||||
def __init__(self, name: str, sourcedir: str = "") -> None:
|
||||
super().__init__(name, sources=[])
|
||||
self.sourcedir = os.fspath(Path(sourcedir).resolve())
|
||||
|
||||
|
||||
class CMakeBuild(build_ext):
|
||||
def build_extension(self, ext: CMakeExtension) -> None:
|
||||
# Must be in this form due to bug in .resolve() only fixed in Python 3.10+
|
||||
ext_fullpath = Path.cwd() / self.get_ext_fullpath(ext.name) # type: ignore[no-untyped-call]
|
||||
extdir = ext_fullpath.parent.resolve()
|
||||
|
||||
# Using this requires trailing slash for auto-detection & inclusion of
|
||||
# auxiliary "native" libs
|
||||
|
||||
debug = int(os.environ.get("DEBUG", 0)) if self.debug is None else self.debug
|
||||
cfg = "Debug" if debug else "Release"
|
||||
|
||||
# CMake lets you override the generator - we need to check this.
|
||||
# Can be set with Conda-Build, for example.
|
||||
cmake_generator = os.environ.get("CMAKE_GENERATOR", "")
|
||||
|
||||
# Set Python_EXECUTABLE instead if you use PYBIND11_FINDPYTHON
|
||||
# TAILSCALE_VERSION_INFO shows you how to pass a value into the C++ code
|
||||
# from Python.
|
||||
cmake_args = [
|
||||
f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={extdir}{os.sep}",
|
||||
f"-DPYTHON_EXECUTABLE={sys.executable}",
|
||||
f"-DCMAKE_BUILD_TYPE={cfg}", # not used on MSVC, but no harm
|
||||
]
|
||||
build_args = []
|
||||
# Adding CMake arguments set as environment variable
|
||||
# (needed e.g. to build for ARM OSx on conda-forge)
|
||||
if "CMAKE_ARGS" in os.environ:
|
||||
cmake_args += [item for item in os.environ["CMAKE_ARGS"].split(" ") if item]
|
||||
|
||||
# In this example, we pass in the version to C++. You might not need to.
|
||||
cmake_args += [f"-DTAILSCALE_VERSION_INFO={self.distribution.get_version()}"] # type: ignore[attr-defined]
|
||||
|
||||
if self.compiler.compiler_type != "msvc":
|
||||
# Using Ninja-build since it a) is available as a wheel and b)
|
||||
# multithreads automatically. MSVC would require all variables be
|
||||
# exported for Ninja to pick it up, which is a little tricky to do.
|
||||
# Users can override the generator with CMAKE_GENERATOR in CMake
|
||||
# 3.15+.
|
||||
if not cmake_generator or cmake_generator == "Ninja":
|
||||
try:
|
||||
import ninja
|
||||
|
||||
ninja_executable_path = Path(ninja.BIN_DIR) / "ninja"
|
||||
cmake_args += [
|
||||
"-GNinja",
|
||||
f"-DCMAKE_MAKE_PROGRAM:FILEPATH={ninja_executable_path}",
|
||||
]
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
else:
|
||||
# Single config generators are handled "normally"
|
||||
single_config = any(x in cmake_generator for x in {"NMake", "Ninja"})
|
||||
|
||||
# CMake allows an arch-in-generator style for backward compatibility
|
||||
contains_arch = any(x in cmake_generator for x in {"ARM", "Win64"})
|
||||
|
||||
# Specify the arch if using MSVC generator, but only if it doesn't
|
||||
# contain a backward-compatibility arch spec already in the
|
||||
# generator name.
|
||||
if not single_config and not contains_arch:
|
||||
cmake_args += ["-A", PLAT_TO_CMAKE[self.plat_name]]
|
||||
|
||||
# Multi-config generators have a different way to specify configs
|
||||
if not single_config:
|
||||
cmake_args += [
|
||||
f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{cfg.upper()}={extdir}"
|
||||
]
|
||||
build_args += ["--config", cfg]
|
||||
|
||||
if sys.platform.startswith("darwin"):
|
||||
# Cross-compile support for macOS - respect ARCHFLAGS if set
|
||||
archs = re.findall(r"-arch (\S+)", os.environ.get("ARCHFLAGS", ""))
|
||||
if archs:
|
||||
cmake_args += ["-DCMAKE_OSX_ARCHITECTURES={}".format(";".join(archs))]
|
||||
|
||||
# Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level
|
||||
# across all generators.
|
||||
if "CMAKE_BUILD_PARALLEL_LEVEL" not in os.environ:
|
||||
# self.parallel is a Python 3 only way to set parallel jobs by hand
|
||||
# using -j in the build_ext call, not supported by pip or PyPA-build.
|
||||
if hasattr(self, "parallel") and self.parallel:
|
||||
# CMake 3.12+ only.
|
||||
build_args += [f"-j{self.parallel}"]
|
||||
|
||||
build_temp = Path(self.build_temp) / ext.name
|
||||
if not build_temp.exists():
|
||||
build_temp.mkdir(parents=True)
|
||||
|
||||
subprocess.run(
|
||||
["cmake", ext.sourcedir, *cmake_args], cwd=build_temp, check=True
|
||||
)
|
||||
subprocess.run(
|
||||
["cmake", "--build", ".", *build_args], cwd=build_temp, check=True
|
||||
)
|
||||
|
||||
|
||||
setup(
|
||||
name="tailscale",
|
||||
version="0.0.1",
|
||||
author="Tailscale Inc & AUTHORS",
|
||||
author_email="support@tailscale.com",
|
||||
description="Embedded Tailscale",
|
||||
long_description="",
|
||||
ext_modules=[CMakeExtension("tailscale._tailscale")],
|
||||
packages=["tailscale"],
|
||||
cmdclass={"build_ext": CMakeBuild},
|
||||
zip_safe=False,
|
||||
extras_require={"test": ["pytest>=6.0"]},
|
||||
python_requires=">=3.7",
|
||||
)
|
||||
92
python/src/main.cpp
Normal file
92
python/src/main.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
#include "libtailscale.h"
|
||||
|
||||
#define STRINGIFY(x) #x
|
||||
#define MACRO_STRINGIFY(x) STRINGIFY(x)
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
PYBIND11_MODULE(_tailscale, m) {
|
||||
m.doc() = R"pbdoc(
|
||||
Embedded Tailscale
|
||||
-----------------------
|
||||
|
||||
.. currentmodule:: _tailscale
|
||||
|
||||
.. autosummary::
|
||||
:toctree: _generate
|
||||
)pbdoc";
|
||||
|
||||
m.def("new", &TsnetNewServer, R"pbdoc(
|
||||
Create a new tsnet server
|
||||
)pbdoc");
|
||||
|
||||
m.def("start", &TsnetStart, R"pbdoc(
|
||||
Starts a tsnet server
|
||||
)pbdoc");
|
||||
|
||||
m.def("up", &TsnetUp, R"pbdoc(
|
||||
Brings the given tsnet server up
|
||||
)pbdoc");
|
||||
|
||||
m.def("close", &TsnetClose, R"pbdoc(
|
||||
Closes a given tsnet server
|
||||
)pbdoc");
|
||||
|
||||
m.def("err_msg", &TsnetErrmsg, R"pbdoc(
|
||||
|
||||
)pbdoc");
|
||||
|
||||
m.def("listen", [](int sd, char* network, char* addr) { int listenerOut; int rv = TsnetListen(sd, network, addr, &listenerOut); return std::make_tuple(listenerOut, rv); }, R"pbdoc(
|
||||
Listen on a given protocol and port
|
||||
)pbdoc");
|
||||
|
||||
m.def("close_listener", &TsnetListenerClose, R"pbdoc(
|
||||
Create a new tsnet server
|
||||
)pbdoc");
|
||||
|
||||
m.def("accept", [](int ld) { int connOut; int rv = TsnetAccept(ld, &connOut); return std::make_tuple(connOut, rv);}, R"pbdoc(
|
||||
Accept a given listener and connection
|
||||
)pbdoc");
|
||||
|
||||
m.def("dial", &TsnetDial, R"pbdoc(
|
||||
|
||||
)pbdoc");
|
||||
|
||||
m.def("set_dir", &TsnetSetDir, R"pbdoc(
|
||||
|
||||
)pbdoc");
|
||||
|
||||
m.def("set_hostname", &TsnetSetHostname, R"pbdoc(
|
||||
|
||||
)pbdoc");
|
||||
|
||||
m.def("set_authkey", &TsnetSetAuthKey, R"pbdoc(
|
||||
|
||||
)pbdoc");
|
||||
|
||||
m.def("set_control_url", &TsnetSetControlURL, R"pbdoc(
|
||||
|
||||
)pbdoc");
|
||||
|
||||
m.def("set_ephemeral", &TsnetSetEphemeral, R"pbdoc(
|
||||
Set the given tsnet server to be an ephemeral node.
|
||||
)pbdoc");
|
||||
|
||||
m.def("set_log_fd", &TsnetSetLogFD, R"pbdoc(
|
||||
|
||||
)pbdoc");
|
||||
|
||||
m.def("loopback_api", &TsnetLoopbackAPI, R"pbdoc(
|
||||
|
||||
)pbdoc");
|
||||
|
||||
#ifdef VERSION_INFO
|
||||
m.attr("__version__") = MACRO_STRINGIFY(VERSION_INFO);
|
||||
#else
|
||||
m.attr("__version__") = "dev";
|
||||
#endif
|
||||
}
|
||||
4
python/tailscale/__init__.py
Normal file
4
python/tailscale/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
from .tsnet import TSNet, TSNetException
|
||||
44
python/tailscale/tsnet.py
Normal file
44
python/tailscale/tsnet.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
import io
|
||||
|
||||
from . import _tailscale
|
||||
|
||||
class TSNetException(Exception): pass
|
||||
|
||||
|
||||
class TSNet:
|
||||
def __init__(self, ephemeral=False):
|
||||
self.ts = _tailscale.new()
|
||||
if ephemeral and _tailscale.set_ephemeral(self.ts, 1):
|
||||
raise TSNetException("Error setting ephemeral")
|
||||
|
||||
def up(self):
|
||||
if _tailscale.up(self.ts):
|
||||
raise TSNetException("Error coming up")
|
||||
|
||||
def listen(self, proto, addr):
|
||||
ln, err = _tailscale.listen(self.ts, proto, addr)
|
||||
if err:
|
||||
raise TSNetException("Error listening: %s on %s" % (proto, addr))
|
||||
return TSNetListener(ln)
|
||||
|
||||
def close(self):
|
||||
if _tailscale.close(self.ts):
|
||||
raise TSNetException("Failed to close")
|
||||
|
||||
|
||||
class TSNetListener:
|
||||
def __init__(self, ln):
|
||||
self.ln = ln
|
||||
|
||||
def accept(self):
|
||||
fd, err = _tailscale.accept(self.ln)
|
||||
if err:
|
||||
raise TSNetException("Failed to accept conn")
|
||||
return fd
|
||||
|
||||
def close(self):
|
||||
if _tailscale.close_listener(self.ln):
|
||||
raise TSNetException("Failed to close")
|
||||
Reference in New Issue
Block a user