From b955d2aea544952d639eb3b96713b5dc264470bd Mon Sep 17 00:00:00 2001 From: jwijenbergh Date: Wed, 23 Oct 2024 11:31:39 +0200 Subject: [PATCH] WIP --- client/client.go | 8 -- client/proxy.go | 125 ------------------------ client/proxy_test.go | 36 ------- exports/exports.go | 82 +++++++++++----- exports/exports.h | 11 +-- go.mod | 2 + internal/api/api_test.go | 4 +- internal/server/server.go | 2 +- internal/wireguard/wireguard.go | 15 +-- proxy/proxy.go | 59 +++++++++++ types/server/server.go | 5 +- wrappers/python/eduvpn_common/loader.py | 23 ++++- wrappers/python/eduvpn_common/main.py | 40 +++++--- wrappers/python/eduvpn_common/types.py | 27 ++++- 14 files changed, 208 insertions(+), 231 deletions(-) delete mode 100644 client/proxy.go delete mode 100644 client/proxy_test.go create mode 100644 proxy/proxy.go diff --git a/client/client.go b/client/client.go index e5a39c07..e8fc02ce 100644 --- a/client/client.go +++ b/client/client.go @@ -49,9 +49,6 @@ type Client struct { // cfg is the config cfg *config.Config - // proxy is proxyguard - proxy Proxy - mu sync.Mutex discoMan *discovery.Manager @@ -557,11 +554,6 @@ func (c *Client) retrieveTokens(sid string, t srvtypes.Type) (*eduoauth.Token, e // Cleanup cleans up the VPN connection by sending a /disconnect func (c *Client) Cleanup(ck *cookie.Cookie) error { defer c.TrySave() - // cleanup proxyguard - cerr := c.proxy.Cancel() - if cerr != nil { - log.Logger.Debugf("ProxyGuard cancel gave an error: %v", cerr) - } srv, err := c.Servers.CurrentServer() if err != nil { return i18nerr.WrapInternal(err, "The current server was not found when cleaning up the connection") diff --git a/client/proxy.go b/client/proxy.go deleted file mode 100644 index 2a900e06..00000000 --- a/client/proxy.go +++ /dev/null @@ -1,125 +0,0 @@ -package client - -import ( - "context" - "encoding/json" - "errors" - "sync" - - "codeberg.org/eduVPN/proxyguard" - - "github.com/eduvpn/eduvpn-common/i18nerr" - httpw "github.com/eduvpn/eduvpn-common/internal/http" - "github.com/eduvpn/eduvpn-common/internal/log" - "github.com/eduvpn/eduvpn-common/types/cookie" -) - -// ProxyLogger is defined here such that we can update the proxyguard logger -type ProxyLogger struct{} - -// Logf logs a message with parameters -func (pl *ProxyLogger) Logf(msg string, params ...interface{}) { - log.Logger.Infof("[Proxyguard] "+msg, params...) -} - -// Log logs a message -func (pl *ProxyLogger) Log(msg string) { - log.Logger.Infof("[Proxyguard] %s", msg) -} - -// Proxy is a wrapper around ProxyGuard -// that has the client -// and a cancel for cancellation by common -// and a mutex to protect against race conditions -type Proxy struct { - c *proxyguard.Client - mu sync.Mutex - cancel context.CancelFunc -} - -// NewClient creates a new ProxyGuard wrapper from client `c` -func (p *Proxy) NewClient(c *proxyguard.Client) { - p.mu.Lock() - defer p.mu.Unlock() - p.c = c -} - -// Delete sets the inner client to nil -func (p *Proxy) Delete() { - p.mu.Lock() - defer p.mu.Unlock() - p.c = nil -} - -// ErrNoProxyGuardCancel indicates that no ProxyGuard cancel function -// was ever defined. You probably forgot to call `Tunnel` -var ErrNoProxyGuardCancel = errors.New("no ProxyGuard cancel function") - -// Cancel cancels a running ProxyGuard tunnel -// it returns an error if it cannot be canceled -func (p *Proxy) Cancel() error { - p.mu.Lock() - defer p.mu.Unlock() - if p.cancel == nil { - return ErrNoProxyGuardCancel - } - p.cancel() - p.cancel = nil - return nil -} - -// ErrNoProxyGuardClient is an error that is returned when no ProxyGuard client is created -var ErrNoProxyGuardClient = errors.New("no ProxyGuard client created") - -// Tunnel is a wrapper around ProxyGuard tunnel that -// that creates a new context that can be canceled -func (p *Proxy) Tunnel(ctx context.Context, peer string) error { - p.mu.Lock() - if p.c == nil { - p.mu.Unlock() - return ErrNoProxyGuardClient - } - cctx, cf := context.WithCancel(ctx) - p.cancel = cf - client := *p.c - p.mu.Unlock() - defer func() { - p.mu.Lock() - p.cancel = nil - p.mu.Unlock() - }() - // we set peer IPs to nil here as proxyguard already does a DNS request for us - return client.Tunnel(cctx, peer, nil) -} - -// StartProxyguard starts proxyguard for proxied WireGuard connections -func (c *Client) StartProxyguard(ck *cookie.Cookie, listen string, tcpsp int, peer string, gotFD func(fd int, pips string), ready func()) error { - var err error - proxyguard.UpdateLogger(&ProxyLogger{}) - - proxyc := proxyguard.Client{ - Listen: listen, - TCPSourcePort: tcpsp, - SetupSocket: func(fd int, pips []string) { - if gotFD == nil { - return - } - b, err := json.Marshal(pips) - if err != nil { - log.Logger.Errorf("marshalling peer IPs failed: %v", err) - return - } - gotFD(fd, string(b)) - }, - UserAgent: httpw.UserAgent, - Ready: ready, - } - - c.proxy.NewClient(&proxyc) - defer c.proxy.Delete() - err = c.proxy.Tunnel(ck.Context(), peer) - if err != nil { - return i18nerr.WrapInternal(err, "The VPN proxy exited") - } - return err -} diff --git a/client/proxy_test.go b/client/proxy_test.go deleted file mode 100644 index ddb0c4fb..00000000 --- a/client/proxy_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package client - -import ( - "context" - "errors" - "testing" - - "codeberg.org/eduVPN/proxyguard" -) - -func TestProxy(t *testing.T) { - // test race - p := Proxy{} - p.NewClient(&proxyguard.Client{}) - go func() { - // connect to localhost will fail - // but we don't care about the error - _ = p.Tunnel(context.Background(), "127.0.0.1") - }() - // race! - _ = p.Cancel() - - // cancel before tunneling - p.NewClient(&proxyguard.Client{}) - if !errors.Is(p.Cancel(), ErrNoProxyGuardCancel) { - t.Fatalf("proxyguard cancel err not equal") - } - _ = p.Tunnel(context.Background(), "127.0.0.1") - p.Delete() - - // tunnel without client - gerr := p.Tunnel(context.Background(), "127.0.0.1") - if !errors.Is(gerr, ErrNoProxyGuardClient) { - t.Fatalf("no proxyguard client err not equal") - } -} diff --git a/exports/exports.go b/exports/exports.go index 511a1f98..6df8c332 100644 --- a/exports/exports.go +++ b/exports/exports.go @@ -29,6 +29,7 @@ import ( "github.com/eduvpn/eduvpn-common/client" "github.com/eduvpn/eduvpn-common/i18nerr" "github.com/eduvpn/eduvpn-common/internal/log" + "github.com/eduvpn/eduvpn-common/proxy" "github.com/eduvpn/eduvpn-common/types/cookie" errtypes "github.com/eduvpn/eduvpn-common/types/error" srvtypes "github.com/eduvpn/eduvpn-common/types/server" @@ -876,50 +877,85 @@ func StartFailover(c C.uintptr_t, gateway *C.char, mtu C.int, readRxBytes C.Read return droppedC, nil } -// StartProxyguard starts the 'proxyguard' procedure in eduvpn-common. +// NewProxyguard creates the 'proxyguard' procedure in eduvpn-common. // eduvpn-common currently also cleans up the running ProxyGuard process in `cleanup`. -// If the proxy cannot be started it returns an error. +// If the proxy cannot be created it returns an error. // // This function proxies WireGuard UDP connections over HTTP: [ProxyGuard on Codeberg](https://codeberg.org/eduvpn/proxyguard). // // These input variables can be gotten from the configuration that is retrieved using the `proxy` JSON key // // - `c` is the cookie. Note that if you cancel/delete the cookie, ProxyGuard gets cleaned up. Common automatically cleans up ProxyGuard when `Cleanup` is called, but it is good to cleanup yourself too. -// - `listen` is the `ip:port` of the local udp connection, this is what is set to the WireGuard endpoint +// - `lp` is the `port` of the local udp ProxyGuard connection, this is what is set to the WireGuard endpoint // - `tcpsp` is the TCP source port. Pass 0 if you do not route based on source port, so far only the Linux client has to pass non-zero. // - `peer` is the `ip:port` of the remote server // - `proxySetup` is a callback which is called when the socket is setting up, this can be used for configuring routing in the client. It takes two arguments: the file descriptor (integer) and a JSON list of IPs the client connects to -// - `proxyReady` is a callback when the proxy is ready to be used. This is only called when the client is not connected yet. Use this to determine when the actual wireguard connection can be started. This callback returns and takes no arguments // -// Example Input: ```StartProxyGuard(myCookie, "127.0.0.1:1337", 0, "5.5.5.5:51820", proxySetupCB, proxyReadyCB)``` +// Example Input: ```StartProxyGuard(myCookie, 1337, 0, "5.5.5.5:51820", proxySetupCB)``` // // Example Output: ```null``` // -//export StartProxyguard -func StartProxyguard(c C.uintptr_t, listen *C.char, tcpsp C.int, peer *C.char, proxySetup C.ProxySetup, proxyReady C.ProxyReady) *C.char { - state, stateErr := getVPNState() - if stateErr != nil { - return getCError(stateErr) - } +//export NewProxyguard +func NewProxyguard(c C.uintptr_t, lp C.int, tcpsp C.int, peer *C.char, proxySetup C.ProxySetup) (C.uintptr_t, *C.char) { ck, err := getCookie(c) if err != nil { - return getCError(err) + return 0, getCError(err) } - - proxyErr := state.StartProxyguard(ck, C.GoString(listen), int(tcpsp), C.GoString(peer), func(fd int, pips string) { + proxy, proxyErr := proxy.NewProxyguard(ck.Context(), int(lp), int(tcpsp), C.GoString(peer), func(fd int) { if proxySetup == nil { return } - cpip := C.CString(pips) - C.call_proxy_setup(proxySetup, C.int(fd), cpip) - FreeString(cpip) - }, func() { - if proxyReady == nil { - return - } - C.call_proxy_ready(proxyReady) + C.call_proxy_setup(proxySetup, C.int(fd)) }) - return getCError(proxyErr) + if proxyErr != nil { + return 0, getCError(proxyErr) + } + return C.uintptr_t(cgo.NewHandle(proxy)), nil +} + +func getProxy(proxyH C.uintptr_t) (*proxy.Proxy, error) { + h := cgo.Handle(proxyH) + v, ok := h.Value().(*proxy.Proxy) + if !ok { + return nil, i18nerr.NewInternal("value is not a proxyguard wrapper") + } + return v, nil +} + +//export ProxyguardTunnel +func ProxyguardTunnel(c C.uintptr_t, proxyH C.uintptr_t, wglisten C.int) *C.char { + ck, err := getCookie(c) + if err != nil { + return getCError(err) + } + pr, err := getProxy(proxyH) + if err != nil { + return getCError(err) + } + tunnelErr := pr.Tunnel(ck.Context(), int(wglisten)) + + // after tunneling is done, the handle should be deleted + cgo.Handle(proxyH).Delete() + return getCError(tunnelErr) +} + +//export ProxyguardPeerIPs +func ProxyguardPeerIPs(proxyH C.uintptr_t) (*C.char, *C.char) { + pr, err := getProxy(proxyH) + if err != nil { + return nil, getCError(err) + } + pips := pr.PeerIPS + + b, err := json.Marshal(pips) + if err != nil { + return nil, getCError(i18nerr.WrapInternal(err, "failed converting Peer IPs to JSON")) + } + ret, err := getReturnData(string(b)) + if err != nil { + return nil, getCError(err) + } + return C.CString(ret), nil } // SetState sets the state of the state machine. diff --git a/exports/exports.h b/exports/exports.h index a31920be..9ec39e41 100644 --- a/exports/exports.h +++ b/exports/exports.h @@ -11,8 +11,7 @@ typedef int (*StateCB)(int oldstate, int newstate, void* data); typedef void (*RefreshList)(); typedef void (*TokenGetter)(const char* server_id, int server_type, char* out, size_t len); typedef void (*TokenSetter)(const char* server_id, int server_type, const char* tokens); -typedef void (*ProxySetup)(int fd, const char* peer_ips); -typedef void (*ProxyReady)(); +typedef void (*ProxySetup)(int fd); static long long int get_read_rx_bytes(ReadRxBytes read) { @@ -34,13 +33,9 @@ static void call_token_setter(TokenSetter setter, const char* server_id, int ser { setter(server_id, server_type, tokens); } -static void call_proxy_setup(ProxySetup proxysetup, int fd, const char* peer_ips) +static void call_proxy_setup(ProxySetup proxysetup, int fd) { - proxysetup(fd, peer_ips); -} -static void call_proxy_ready(ProxyReady ready) -{ - ready(); + proxysetup(fd); } #endif /* EXPORTS_H */ diff --git a/go.mod b/go.mod index b9a6225a..15314b95 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module github.com/eduvpn/eduvpn-common go 1.18 +replace codeberg.org/eduVPN/proxyguard => ../proxyguard + require ( codeberg.org/eduVPN/proxyguard v0.0.0-20240924084349-c0250730030d github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 diff --git a/internal/api/api_test.go b/internal/api/api_test.go index f15bac4e..9648544b 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -497,8 +497,8 @@ PrivateKey = .*`, if gcd.Proxy.SourcePort <= 0 { t.Fatalf("got proxy source port is smaller or equal to 0: %v", gcd.Proxy.SourcePort) } - if !strings.Contains(gcd.Proxy.Listen, "127.0.0.1") { - t.Fatalf("proxy listen does not contain 127.0.0.1: %s", gcd.Proxy.Listen) + if gcd.Proxy.ListenPort <= 0 { + t.Fatalf("proxy listen port is smaller or equal to 0: %v", gcd.Proxy.ListenPort) } c.cd.Proxy = gcd.Proxy } diff --git a/internal/server/server.go b/internal/server/server.go index eac1c2a7..6bbca68f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -142,7 +142,7 @@ func (s *Server) connect(ctx context.Context, pTCP bool) (*srvtypes.Configuratio if apicfg.Proxy != nil { proxy = &srvtypes.Proxy{ SourcePort: apicfg.Proxy.SourcePort, - Listen: apicfg.Proxy.Listen, + ListenPort: apicfg.Proxy.ListenPort, Peer: apicfg.Proxy.Peer, } } diff --git a/internal/wireguard/wireguard.go b/internal/wireguard/wireguard.go index a70f21a3..92398352 100644 --- a/internal/wireguard/wireguard.go +++ b/internal/wireguard/wireguard.go @@ -40,8 +40,8 @@ func availableUDPPort() (int, error) { type Proxy struct { // SourcePort is the source port of the TCP socket SourcePort int - // Listen is the IP:PORT of the udp listener - Listen string + // ListenPort is the PORT of the udp listener + ListenPort int // Peer is the hostname/ip:port of the WireGuard peer Peer string } @@ -54,22 +54,23 @@ func Config(cfg string, key *wgtypes.Key, proxy bool) (string, *Proxy, error) { } var tcpp int - var plisten string + var udpp int var err error + var udpl string if proxy { tcpp, err = availableTCPPort() if err != nil { return "", nil, err } - udpp, err := availableUDPPort() + udpp, err = availableUDPPort() if err != nil { return "", nil, err } - plisten = fmt.Sprintf("127.0.0.1:%d", udpp) + udpl = fmt.Sprintf("127.0.0.1:%d", udpp) } - rcfg, peer, err := configReplace(cfg, *key, plisten) + rcfg, peer, err := configReplace(cfg, *key, udpl) if err != nil { return "", nil, err } @@ -77,7 +78,7 @@ func Config(cfg string, key *wgtypes.Key, proxy bool) (string, *Proxy, error) { if proxy { retP = &Proxy{ SourcePort: tcpp, - Listen: plisten, + ListenPort: udpp, Peer: peer, } } diff --git a/proxy/proxy.go b/proxy/proxy.go new file mode 100644 index 00000000..f2304ea7 --- /dev/null +++ b/proxy/proxy.go @@ -0,0 +1,59 @@ +// package proxy is a wrapper around proxyguard that integrates it with eduvpn-common settings +// - leaves out some options not applicable to the common integration, e.g. fwmark +// - integrates with eduvpn-common's logger +// - integrates eduvpn-common's user agent +package proxy + +import ( + "context" + "codeberg.org/eduVPN/proxyguard" + + "github.com/eduvpn/eduvpn-common/i18nerr" + httpw "github.com/eduvpn/eduvpn-common/internal/http" + "github.com/eduvpn/eduvpn-common/internal/log" +) + +// Logger is defined here such that we can update the proxyguard logger +type Logger struct{} + +// Logf logs a message with parameters +func (l *Logger) Logf(msg string, params ...interface{}) { + log.Logger.Infof("[Proxyguard] "+msg, params...) +} + +// Log logs a message +func (l *Logger) Log(msg string) { + log.Logger.Infof("[Proxyguard] %s", msg) +} + +type Proxy struct { + proxyguard.Client +} + +// NewProxyguard sets up proxyguard for proxied WireGuard connections +func NewProxyguard(ctx context.Context, lp int, tcpsp int, peer string, setupSocket func(fd int)) (*Proxy, error) { + proxyguard.UpdateLogger(&Logger{}) + proxy := Proxy{ + proxyguard.Client { + Peer: peer, + ListenPort: lp, + TCPSourcePort: tcpsp, + SetupSocket: setupSocket, + UserAgent: httpw.UserAgent, + }, + } + err := proxy.Client.SetupDNS(ctx) + if err != nil { + return nil, i18nerr.WrapInternal(err, "The ProxyGuard DNS could not be resolved") + } + + return &proxy, nil +} + +func (p *Proxy) Tunnel(ctx context.Context, wglisten int) error { + err := p.Client.Tunnel(ctx, wglisten) + if err != nil { + return i18nerr.WrapInternal(err, "The VPN proxy exited") + } + return nil +} diff --git a/types/server/server.go b/types/server/server.go index 1d55ea76..b8805435 100644 --- a/types/server/server.go +++ b/types/server/server.go @@ -164,8 +164,9 @@ type List struct { type Proxy struct { // SourcePort is the source port for the client TCP connection SourcePort int `json:"source_port"` - // Listen is the ip:port for the client UDP connection, this is the value that is replaced in the config - Listen string `json:"listen"` + // ListenPort is the port for the client UDP connection, this is the value that is set as the WireGuard endpoint prepended with "127.0.0.1:" + // So if the listen port is set to 1337, the WireGuard endpoint would be 127.0.0.1:1337 + ListenPort int `json:"listen_port"` // Peer is the URI of the upstream server // Note that this exactly matches the "ProxyEndpoint" key in the WireGuard config Peer string `json:"peer"` diff --git a/wrappers/python/eduvpn_common/loader.py b/wrappers/python/eduvpn_common/loader.py index e1631487..8ac6372a 100644 --- a/wrappers/python/eduvpn_common/loader.py +++ b/wrappers/python/eduvpn_common/loader.py @@ -5,7 +5,7 @@ from eduvpn_common.types import ( BoolError, DataError, - ProxyReady, + HandlerError, ProxySetup, ReadRxBytes, RefreshList, @@ -134,14 +134,27 @@ def initialize_functions(lib: CDLL) -> None: ], BoolError, ) - lib.StartProxyguard.argtypes, lib.StartProxyguard.restype = ( + lib.NewProxyguard.argtypes, lib.NewProxyguard.restype = ( [ c_int, - c_char_p, + c_int, c_int, c_char_p, ProxySetup, - ProxyReady, ], - c_void_p, + HandlerError, + ) + lib.ProxyguardTunnel.argtypes, lib.ProxyguardTunnel.restype = ( + [ + c_int, + c_int, + c_int, + ], + c_char_p, + ) + lib.ProxyguardPeerIPs.argtypes, lib.ProxyguardPeerIPs.restype = ( + [ + c_int, + ], + DataError, ) diff --git a/wrappers/python/eduvpn_common/main.py b/wrappers/python/eduvpn_common/main.py index ce520246..04a2302e 100644 --- a/wrappers/python/eduvpn_common/main.py +++ b/wrappers/python/eduvpn_common/main.py @@ -1,13 +1,12 @@ import ctypes import json from enum import IntEnum -from typing import Any, Callable, Iterator, Optional +from typing import Any, Callable, Iterator, List, Optional from eduvpn_common.event import EventHandler from eduvpn_common.loader import initialize_functions, load_lib from eduvpn_common.state import State from eduvpn_common.types import ( - ProxyReady, ProxySetup, ReadRxBytes, RefreshList, @@ -21,6 +20,24 @@ global_object = None +class Proxyguard(object): + def __init__(self, parent, handler): + self.parent = parent + self.handler = handler + + def tunnel(self, wglisten: int): + tunnel_err = self.parent.go_cookie_function(self.parent.lib.ProxyguardTunnel, self.handler, wglisten) + if tunnel_err: + forwardError(tunnel_err) + + @property + def peer_ips(self) -> List[str]: + peer_ips, peer_ips_err = self.parent.go_function(self.parent.lib.ProxyguardPeerIPs, self.handler) + if peer_ips_err: + forwardError(peer_ips_err) + return json.loads(peer_ips) + + class WrappedError(Exception): def __init__(self, translations, language, misc): self.translations = translations @@ -359,24 +376,23 @@ def start_failover(self, gateway: str, wg_mtu: int, readrxbytes: ReadRxBytes) -> forwardError(dropped_err) return dropped - def start_proxyguard( + def new_proxyguard( self, - listen: str, - source_port: int, + listen_port: int, + tcp_source_port: int, peer: str, setup: ProxySetup, - ready: ProxyReady, - ): - proxy_err = self.go_cookie_function( - self.lib.StartProxyguard, - listen, - source_port, + ) -> Proxyguard: + proxy, proxy_err = self.go_cookie_function( + self.lib.NewProxyguard, + listen_port, + tcp_source_port, peer, setup, - ready, ) if proxy_err: forwardError(proxy_err) + return Proxyguard(self, proxy) def cancel(self): self.jar.cancel() diff --git a/wrappers/python/eduvpn_common/types.py b/wrappers/python/eduvpn_common/types.py index 716556e2..8c0d9a24 100644 --- a/wrappers/python/eduvpn_common/types.py +++ b/wrappers/python/eduvpn_common/types.py @@ -24,6 +24,13 @@ class DataError(Structure): _fields_ = [("data", c_void_p), ("error", c_void_p)] +class HandlerError(Structure): + """The C type that represents a tuple of a CGO handler and error (string) as returned by the Go library + + :meta private: + """ + + _fields_ = [("handler", c_int), ("error", c_void_p)] class BoolError(Structure): """The C type that represents a tuple of boolean and error as returned by the Go library @@ -36,8 +43,7 @@ class BoolError(Structure): # The type for a Go state change callback VPNStateChange = CFUNCTYPE(c_int, c_int, c_int, c_char_p) -ProxySetup = CFUNCTYPE(c_void_p, c_int, c_char_p) -ProxyReady = CFUNCTYPE(c_void_p) +ProxySetup = CFUNCTYPE(c_void_p, c_int) ReadRxBytes = CFUNCTYPE(c_ulonglong) RefreshList = CFUNCTYPE(c_void_p) TokenGetter = CFUNCTYPE(c_void_p, c_char_p, c_int, POINTER(c_char), c_size_t) @@ -84,6 +90,7 @@ def decode_res(res: Any) -> Any: """ decode_map = { c_void_p: get_ptr_string, + HandlerError: get_handler_error, DataError: get_data_error, BoolError: get_bool_error, } @@ -126,6 +133,22 @@ def get_data_error(lib: CDLL, data_error: DataError) -> Tuple[str, str]: return data, error +def get_handler_error(lib: CDLL, handler_error: HandlerError) -> Tuple[int, str]: + """Convert a C handler+error structure to a Python usable handler+error structure + + :param lib: CDLL: The Go shared library + :param handler_error: HandlerError: The handler error C structure + + :meta private: + + :return: The handler and error + :rtype: Tuple[int, str] + """ + handler = int(handler_error.handler) + error = get_ptr_string(lib, handler_error.error) + return handler, error + + def get_bool_error(lib: CDLL, bool_error: BoolError) -> Tuple[bool, str]: """Convert a C boolean (c int)+error structure to a Python usable boolean+error structure