diff --git a/examples/python/Makefile b/examples/python/Makefile new file mode 100644 index 0000000..2b7cce3 --- /dev/null +++ b/examples/python/Makefile @@ -0,0 +1,15 @@ +LIB_OUTPUT_DIR = $(shell cd ../../ && ./build.sh gethosttype) + +copy_wrapper_sources: + cp -f ../../src/bindings/python/*.py . + +debug: copy_wrapper_sources + cd ../../ && ./build.sh host-python "debug" + cp -f ../../dist/$(LIB_OUTPUT_DIR)-python-debug/lib/*.so . + +release: copy_wrapper_sources + cd ../../ && ./build.sh host-python "release" + cp -f ../../dist/$(LIB_OUTPUT_DIR)-python-release/lib/*.so . + +clean: + rm -rf *.so libzt.py prototype.py \ No newline at end of file diff --git a/examples/python/README.md b/examples/python/README.md new file mode 100644 index 0000000..640d97b --- /dev/null +++ b/examples/python/README.md @@ -0,0 +1,23 @@ +# Python example + +This example demonstrates how to use the ZeroTier socket interface provided by libzt in a Python application. The API is designed to be a drop-in replacement for the Python [Low-level networking interface](https://docs.python.org/3/library/socket.html). + +Note: Only `AF_INET` and `AF_INET6` address families are supported. + +### Install + +``` +pip install libzt +``` + +### Run +``` +python3 example.py server id-path/bob 0123456789abcdef 9997 8080 +python3 example.py client id-path/alice 0123456789abcdef 9996 11.22.33.44 8080 +``` + +*Where `9996` and `9997` are arbitrary ports that you allow ZeroTier to use for encrypted UDP traffic, port `8080` is an arbitrary port used by the client/server socket code, and `11.22.33.44` should be whatever IP address the network assigns your node.* + +### Implementation Details + +- See [src/bindings/python](../../src/bindings/python) diff --git a/examples/python/example.py b/examples/python/example.py new file mode 100644 index 0000000..5a4a759 --- /dev/null +++ b/examples/python/example.py @@ -0,0 +1,159 @@ +import time, sys + +import libzt +from prototype import * + +# Where identity files are stored +keyPath = "." + +# Network to join +networkId = 0 + +# Port used by ZeroTier to send encpryted UDP traffic +# NOTE: Should be different from other instances of ZeroTier +# running on the same machine +ztServicePort = 9997 + +remoteIP = None + +# A port your app logic may use +serverPort = 8080 + +# Flags to keep state +is_joined = False +is_online = False +mode = None + +def print_usage(): + print("\nUsage: \n") + print(" Ex: python3 example.py server . 0123456789abcdef 9994 8080") + print(" Ex: python3 example.py client . 0123456789abcdef 9994 192.168.22.1 8080\n") + if (len(sys.argv) < 6): + print('Too few arguments') + if (len(sys.argv) > 7): + print('Too many arguments') + exit(0) +# +if (len(sys.argv) < 6 or len(sys.argv) > 7): + print_usage() + +if (sys.argv[1] == 'server' and len(sys.argv) == 6): + mode = sys.argv[1] + keyPath = sys.argv[2] + networkId = int(sys.argv[3],16) + ztServicePort = int(sys.argv[4]) + serverPort = int(sys.argv[5]) + +if (sys.argv[1] == 'client' and len(sys.argv) == 7): + mode = sys.argv[1] + keyPath = sys.argv[2] + networkId = int(sys.argv[3],16) + ztServicePort = int(sys.argv[4]) + remoteIP = sys.argv[5] + serverPort = int(sys.argv[6]) + +if (mode is None): + print_usage() + +print('mode = ', mode) +print('path = ', keyPath) +print('networkId = ', networkId) +print('ztServicePort = ', ztServicePort) +print('remoteIP = ', remoteIP) +print('serverPort = ', serverPort) + + + +# +# Event handler +# +class MyEventCallbackClass(libzt.PythonDirectorCallbackClass): + def on_zerotier_event(self, msg): + global is_online + global is_joined + print("eventCode=", msg.eventCode) + if (msg.eventCode == libzt.ZTS_EVENT_NODE_ONLINE): + print("ZTS_EVENT_NODE_ONLINE") + print("nodeId="+hex(msg.node.address)) + # The node is now online, you can join/leave networks + is_online = True + if (msg.eventCode == libzt.ZTS_EVENT_NODE_OFFLINE): + print("ZTS_EVENT_NODE_OFFLINE") + if (msg.eventCode == libzt.ZTS_EVENT_NETWORK_READY_IP4): + print("ZTS_EVENT_NETWORK_READY_IP4") + is_joined = True + # The node has successfully joined a network and has an address + # you can perform network calls now + if (msg.eventCode == libzt.ZTS_EVENT_PEER_DIRECT): + print("ZTS_EVENT_PEER_DIRECT") + if (msg.eventCode == libzt.ZTS_EVENT_PEER_RELAY): + print("ZTS_EVENT_PEER_RELAY") + + + +# +# Example start and join logic +# +print("Starting ZeroTier..."); +eventCallback = MyEventCallbackClass() +libzt.zts_start(keyPath, eventCallback, ztServicePort) +print("Waiting for node to come online...") +while (not is_online): + time.sleep(1) +print("Joining network:", hex(networkId)); +libzt.zts_join(networkId) +while (not is_joined): + time.sleep(1) # You can ping this app at this point +print('Joined network') + + + +# +# Example server +# +if (mode == 'server'): + print("Starting server...") + try: + serv = zerotier.socket(libzt.ZTS_AF_INET6, libzt.ZTS_SOCK_STREAM, 0) + serv.bind(('::', serverPort)) + serv.listen(5) + while True: + conn, addr = serv.accept() + print('Accepted connection from: ', addr) + while True: + print('recv()...') + data = conn.recv(4096) + if data: + print('data = ', data) + #print(type(b'what')) + #exit(0) + if not data: break + print('send()...') + #bytes(data, 'ascii') + b'\x00' + n_bytes = conn.send(data) # echo back to the server + print('sent ' + str(n_bytes) + ' byte(s)') + conn.close() + print('client disconnected') + except Exception as e: + print(e) + + +# +# Example client +# +if (mode == 'client'): + print("Starting client...") + try: + client = zerotier.socket(libzt.ZTS_AF_INET6, libzt.ZTS_SOCK_STREAM, 0) + print("connecting...") + client.connect((remoteIP, serverPort)) + print("send...") + data = 'Hello, world!' + client.send(data) + print("rx...") + data = client.recv(1024) + + print('Received', repr(data)) + except Exception as e: + print(e) + diff --git a/examples/python/zt.i b/examples/python/zt.i deleted file mode 100644 index 69aa836..0000000 --- a/examples/python/zt.i +++ /dev/null @@ -1,30 +0,0 @@ -/* libzt.i */ - -%begin -%{ -#define SWIG_PYTHON_CAST_MODE -%} - -%include - -#define PYTHON_BUILD 1 - -%module libzt -%{ -#include "../include/ZeroTier.h" -#include "../include/ZeroTierConstants.h" -%} - -%define %cs_callback(TYPE, CSTYPE) - %typemap(ctype) TYPE, TYPE& "void *" - %typemap(in) TYPE %{ $1 = ($1_type)$input; %} - %typemap(in) TYPE& %{ $1 = ($1_type)&$input; %} - %typemap(imtype, out="IntPtr") TYPE, TYPE& "CSTYPE" - %typemap(cstype, out="IntPtr") TYPE, TYPE& "CSTYPE" - %typemap(csin) TYPE, TYPE& "$csinput" -%enddef - -%cs_callback(userCallbackFunc, CSharpCallback) - -%include "../include/ZeroTier.h" -%include "../include/ZeroTierConstants.h" diff --git a/src/bindings/python/PythonSockets.cpp b/src/bindings/python/PythonSockets.cpp new file mode 100644 index 0000000..1741168 --- /dev/null +++ b/src/bindings/python/PythonSockets.cpp @@ -0,0 +1,148 @@ +/* + * Copyright (c)2013-2021 ZeroTier, Inc. + * + * Use of this software is governed by the Business Source License included + * in the LICENSE.TXT file in the project's root directory. + * + * Change Date: 2025-01-01 + * + * On the date above, in accordance with the Business Source License, use + * of this software will be governed by version 2.0 of the Apache License. + */ +/****/ + +/** + * @file + * + * ZeroTier Socket API (Python) + */ + +#include "lwip/sockets.h" +#include "ZeroTierSockets.h" + +#ifdef ZTS_ENABLE_PYTHON + +static int tuple_to_sockaddr(int family, + PyObject *addr_obj, struct zts_sockaddr *dst_addr, int *addrlen) +{ + if (family == AF_INET) { + struct zts_sockaddr_in* addr; + char *host_str; + int result, port; + if (!PyTuple_Check(addr_obj)) { + return ZTS_ERR_ARG; + } + if (!PyArg_ParseTuple(addr_obj, + "eti:tuple_to_sockaddr", "idna", &host_str, &port)) { + return ZTS_ERR_ARG; + } + addr = (struct zts_sockaddr_in*)dst_addr; + addr->sin_addr.s_addr = zts_inet_addr(host_str); + PyMem_Free(host_str); + if (port < 0 || port > 0xFFFF) { + return ZTS_ERR_ARG; + } + if (result < 0) { + return ZTS_ERR_ARG; + } + addr->sin_family = AF_INET; + addr->sin_port = htons((short)port); + *addrlen = sizeof *addr; + return ZTS_ERR_OK; + } + if (family == AF_INET6) { + // TODO + } + return ZTS_ERR_ARG; +} + +PyObject * zts_py_accept(int fd) +{ + struct zts_sockaddr_in addrbuf; + socklen_t addrlen = sizeof(addrbuf); + memset(&addrbuf, 0, addrlen); + int err = zts_accept(fd, (struct zts_sockaddr*)&addrbuf, &addrlen); + char ipstr[ZTS_INET_ADDRSTRLEN]; + memset(ipstr, 0, sizeof(ipstr)); + zts_inet_ntop(ZTS_AF_INET, &(addrbuf.sin_addr), ipstr, ZTS_INET_ADDRSTRLEN); + PyObject *t; + t = PyTuple_New(3); + PyTuple_SetItem(t, 0, PyLong_FromLong(err)); // New file descriptor + PyTuple_SetItem(t, 1, PyUnicode_FromString(ipstr)); + PyTuple_SetItem(t, 2, PyLong_FromLong(zts_ntohs(addrbuf.sin_port))); + Py_INCREF(t); + return t; +} + +int zts_py_listen(int fd, int backlog) +{ + return zts_listen(fd, backlog); +} + +int zts_py_bind(int fd, int family, int type, PyObject *addr_obj) +{ + struct zts_sockaddr_storage addrbuf; + int addrlen; + int err; + if (tuple_to_sockaddr(family, addr_obj, + (struct zts_sockaddr *)&addrbuf, &addrlen) != ZTS_ERR_OK) + { + return ZTS_ERR_ARG; + } + Py_BEGIN_ALLOW_THREADS + err = zts_bind(fd, (struct zts_sockaddr *)&addrbuf, addrlen); + Py_END_ALLOW_THREADS + Py_INCREF(Py_None); + return err; +} + +int zts_py_connect(int fd, int family, int type, PyObject *addr_obj) +{ + struct zts_sockaddr_storage addrbuf; + int addrlen; + int err; + if (tuple_to_sockaddr(family, addr_obj, + (struct zts_sockaddr *)&addrbuf, &addrlen) != ZTS_ERR_OK) + { + return ZTS_ERR_ARG; + } + Py_BEGIN_ALLOW_THREADS + err = zts_connect(fd, (struct zts_sockaddr *)&addrbuf, addrlen); + Py_END_ALLOW_THREADS + Py_INCREF(Py_None); + return err; +} + +PyObject * zts_py_recv(int fd, int len, int flags) +{ + PyObject *t; + char buf[4096]; + int err = zts_recv(fd, buf, len, flags); + if (err < 0) { + return NULL; + } + t = PyTuple_New(2); + PyTuple_SetItem(t, 0, PyLong_FromLong(err)); + PyTuple_SetItem(t, 1, PyUnicode_FromString(buf)); + Py_INCREF(t); + return t; +} + +int zts_py_send(int fd, PyObject *buf, int len, int flags) +{ + int err = ZTS_ERR_OK; + PyObject *encodedStr = PyUnicode_AsEncodedString(buf, "UTF-8", "strict"); + if (encodedStr) { + char *bytes = PyBytes_AsString(encodedStr); + err = zts_send(fd, bytes, len, flags); + Py_DECREF(encodedStr); + } + return err; +} + +int zts_py_close(int fd) +{ + return zts_close(fd); +} + +#endif // ZTS_ENABLE_PYTHON diff --git a/src/bindings/python/README.md b/src/bindings/python/README.md new file mode 100644 index 0000000..e08e949 --- /dev/null +++ b/src/bindings/python/README.md @@ -0,0 +1,4 @@ +# Python Language Bindings + + - Install (via [PyPI package](https://pypi.org/project/libzt/)): `pip install libzt` + - Example usage: [examples/python](./../../../examples/python/) diff --git a/src/bindings/python/prototype.py b/src/bindings/python/prototype.py new file mode 100644 index 0000000..bac71ff --- /dev/null +++ b/src/bindings/python/prototype.py @@ -0,0 +1,103 @@ +import libzt + +import time +import struct +import pprint +pp = pprint.PrettyPrinter(width=41, compact=True) + +class zerotier(): + # Create a socket + def socket(sock_family, sock_type, sock_proto=0): + return ztsocket(sock_family, sock_type, sock_proto) + # Convert libzt error code to exception + def handle_error(err): + if (err == libzt.ZTS_ERR_SOCKET): + raise Exception('ZTS_ERR_SOCKET (' + str(err) + ')') + if (err == libzt.ZTS_ERR_SERVICE): + raise Exception('ZTS_ERR_SERVICE (' + str(err) + ')') + if (err == libzt.ZTS_ERR_ARG): + raise Exception('ZTS_ERR_ARG (' + str(err) + ')') + # ZTS_ERR_NO_RESULT isn't strictly an error + #if (err == libzt.ZTS_ERR_NO_RESULT): + # raise Exception('ZTS_ERR_NO_RESULT (' + err + ')') + if (err == libzt.ZTS_ERR_GENERAL): + raise Exception('ZTS_ERR_GENERAL (' + str(err) + ')') + +# ZeroTier pythonic low-level socket class +class ztsocket(): + + _fd = -1 # native layer file descriptor + _family = -1 + _type = -1 + _proto = -1 + _connected = False + _closed = True + _bound = False + + def __init__(self, sock_family=-1, sock_type=-1, sock_proto=-1, sock_fd=None): + self._fd = sock_fd + self._family = sock_family + self._type = sock_type + self._family = sock_family + # Only create native socket if no fd was provided. We may have + # accepted a connection + if (sock_fd == None): + self._fd = libzt.zts_socket(sock_family, sock_type, sock_proto) + + def has_dualstack_ipv6(): + return True + + @property + def family(self): + return _family + + @property + def type(self): + return _type + + # Bind the socket to a local interface address + def bind(self, local_address): + err = libzt.zts_py_bind(self._fd, self._family, self._type, local_address) + if (err < 0): + zerotier.handle_error(err) + + # Connect the socket to a remote address + def connect(self, remote_address): + err = libzt.zts_py_connect(self._fd, self._family, self._type, remote_address) + if (err < 0): + zerotier.handle_error(err) + + # Put the socket in a listening state (with an optional backlog argument) + def listen(self, backlog): + err = libzt.zts_py_listen(self._fd, backlog) + if (err < 0): + zerotier.handle_error(err) + + # Accept connection on the socket + def accept(self): + new_conn_fd, addr, port = libzt.zts_py_accept(self._fd) + if (new_conn_fd < 0): + zerotier.handle_error(acc_fd) + return None + return ztsocket(self._family, self._type, self._proto, new_conn_fd), addr + + # Read data from the socket + def recv(self, n_bytes, flags=0): + err, data = libzt.zts_py_recv(self._fd, n_bytes, flags) + if (err < 0): + zerotier.handle_error(err) + return None + return data + + # Write data to the socket + def send(self, data, flags=0): + err = libzt.zts_py_send(self._fd, data, len(data), flags) + if (err < 0): + zerotier.handle_error(err) + return err + + # Close the socket + def close(self): + err = libzt.zts_py_close(self._fd) + if (err < 0): + zerotier.handle_error(err) diff --git a/src/bindings/python/zt.i b/src/bindings/python/zt.i new file mode 100644 index 0000000..8ddefe5 --- /dev/null +++ b/src/bindings/python/zt.i @@ -0,0 +1,39 @@ +/* libzt.i */ + +%begin +%{ +#define SWIG_PYTHON_CAST_MODE +%} + +%include + +#define ZTS_ENABLE_PYTHON 1 + +%module(directors="1") libzt +%module libzt +%{ +#include "ZeroTierSockets.h" +%} + +%feature("director") PythonDirectorCallbackClass; + +%ignore zts_in6_addr; +%ignore zts_sockaddr; +%ignore zts_in_addr; +%ignore zts_sockaddr_in; +%ignore zts_sockaddr_storage; +%ignore zts_sockaddr_in6; + +%ignore zts_linger; +%ignore zts_accept4; +%ignore zts_ip_mreq; +%ignore zts_in_pktinfo; +%ignore zts_ipv6_mreq; + +%ignore zts_fd_set; +%ignore zts_pollfd; +%ignore zts_nfds_t; +%ignore zts_msghdr; +%ignore zts_inet_addr; + +%include "ZeroTierSockets.h"