From 34ad41c2a7c2a1b426a4b3d76dc074faa7a430f6 Mon Sep 17 00:00:00 2001 From: Mada Date: Fri, 11 Mar 2016 13:18:38 +0800 Subject: [PATCH] Allow communicate with tun file dscription opened by 3rd process --- admin/angel/Core.c | 38 +++++ client/Configurator.c | 21 ++- client/cjdroute2.c | 6 + interface/tuntap/AndroidWrapper.c | 92 +++++++++++++ interface/tuntap/AndroidWrapper.h | 32 +++++ util/events/FileNo.h | 63 +++++++++ util/events/libuv/FileNo.c | 221 ++++++++++++++++++++++++++++++ util/events/libuv/FileNo_admin.c | 92 +++++++++++++ util/events/libuv/FileNo_admin.h | 38 +++++ 9 files changed, 600 insertions(+), 3 deletions(-) create mode 100644 interface/tuntap/AndroidWrapper.c create mode 100644 interface/tuntap/AndroidWrapper.h create mode 100644 util/events/FileNo.h create mode 100644 util/events/libuv/FileNo.c create mode 100644 util/events/libuv/FileNo_admin.c create mode 100644 util/events/libuv/FileNo_admin.h diff --git a/admin/angel/Core.c b/admin/angel/Core.c index 4e6aecf55..de9250840 100644 --- a/admin/angel/Core.c +++ b/admin/angel/Core.c @@ -28,6 +28,7 @@ #include "interface/Iface.h" #include "util/events/UDPAddrIface.h" #include "interface/tuntap/TUNInterface.h" +#include "interface/tuntap/AndroidWrapper.h" #include "interface/UDPInterface_admin.h" #ifdef HAS_ETH_INTERFACE #include "interface/ETHInterface_admin.h" @@ -43,6 +44,10 @@ #include "tunnel/IpTunnel_admin.h" #include "tunnel/RouteGen_admin.h" #include "util/events/EventBase.h" +#ifndef win32 +#include "util/events/libuv/FileNo_admin.h" +#include "util/events/FileNo.h" +#endif #include "util/events/Pipe.h" #include "util/events/Timeout.h" #include "util/Hex.h" @@ -105,9 +110,33 @@ struct Context struct EventBase* base; struct NetCore* nc; struct IpTunnel* ipTunnel; +#ifndef win32 + struct FileNo_admin* fileno; +#endif Identity }; +#ifndef win32 +static void onFileNoReceived(void* vcontext, enum FileNo_Type type, int fileno) +{ + struct Context* ctx = Identity_check((struct Context*) vcontext); + struct Jmp jmp; + Jmp_try(jmp) { + struct Pipe* p = Pipe_forFiles(fileno, fileno, ctx->base, &jmp.handler, ctx->alloc); + p->logger = ctx->logger; + if (type == FileNo_Type_ANDROID) { + struct AndroidWrapper* aw = AndroidWrapper_new(ctx->alloc, ctx->logger); + Iface_plumb(&aw->externalIf, &p->iface); + Iface_plumb(&aw->internalIf, &ctx->nc->tunAdapt->tunIf); + } else { + Iface_plumb(&p->iface, &ctx->nc->tunAdapt->tunIf); + } + } Jmp_catch { + Log_warn(ctx->logger, "Failed to configure fileno [%s]", jmp.message); + } +} +#endif + static void shutdown(void* vcontext) { struct Context* context = Identity_check((struct Context*) vcontext); @@ -198,6 +227,11 @@ void Core_init(struct Allocator* alloc, Iface_plumb(&nc->tunAdapt->ipTunnelIf, &ipTunnel->tunInterface); Iface_plumb(&nc->upper->ipTunnelIf, &ipTunnel->nodeInterface); +#ifndef win32 + struct FileNo_admin* fileno = FileNo_admin_new(admin, alloc, eventBase, + logger, eh, onFileNoReceived); +#endif + // The link between the Pathfinder and the core needs to be asynchronous. struct Pathfinder* pf = Pathfinder_register(alloc, logger, eventBase, rand, admin); struct ASynchronizer* pfAsync = ASynchronizer_new(alloc, eventBase, logger); @@ -230,6 +264,10 @@ void Core_init(struct Allocator* alloc, ctx->base = eventBase; ctx->ipTunnel = ipTunnel; ctx->nc = nc; +#ifndef win32 + ctx->fileno = fileno; + ctx->fileno->userData = ctx; +#endif Admin_registerFunction("Core_exit", adminExit, ctx, true, NULL, admin); diff --git a/client/Configurator.c b/client/Configurator.c index b47e12445..51d0747ee 100644 --- a/client/Configurator.c +++ b/client/Configurator.c @@ -263,13 +263,28 @@ static void tunInterface(Dict* ifaceConf, struct Allocator* tempAlloc, struct Co } // Setup the interface. + int64_t* tunfd = Dict_getInt(ifaceConf, String_CONST("tunfd")); + String* type = Dict_getString(ifaceConf, String_CONST("tunfdType")); String* device = Dict_getString(ifaceConf, String_CONST("tunDevice")); + if (tunfd && *tunfd && !device) { + return; + } + Dict* args = Dict_new(tempAlloc); - if (device) { - Dict_putString(args, String_CONST("desiredTunName"), device, tempAlloc); + if (tunfd && *tunfd) { + Dict_putString(args, String_CONST("path"), device, tempAlloc); + if (type) { + Dict_putString(args, String_CONST("type"), + String_new(type->bytes, tempAlloc), tempAlloc); + } + rpcCall0(String_CONST("FileNo_import"), args, ctx, tempAlloc, NULL, false); + } else { + if (device) { + Dict_putString(args, String_CONST("desiredTunName"), device, tempAlloc); + } + rpcCall0(String_CONST("Core_initTunnel"), args, ctx, tempAlloc, NULL, false); } - rpcCall0(String_CONST("Core_initTunnel"), args, ctx, tempAlloc, NULL, false); } static void ipTunnel(Dict* ifaceConf, struct Allocator* tempAlloc, struct Context* ctx) diff --git a/client/cjdroute2.c b/client/cjdroute2.c index f5cba3e14..8e3ea363a 100644 --- a/client/cjdroute2.c +++ b/client/cjdroute2.c @@ -260,6 +260,12 @@ static int genconf(struct Random* rand, bool eth) " {\n" " // The type of interface (only TUNInterface is supported for now)\n" " \"type\": \"TUNInterface\"\n" + " // The tun device is a file description or not.\n" + " // \"tunfd\" : 0,\n" + " // If tunfd value is not 0, the tunDevice should be used as the pipe path\n" + " // used to transfer the tun file description.\n" + " // The type of tunfd (only \"android\" is supported for now)\n" + " // \"tunfdType\" : \"\"\n" #ifndef __APPLE__ "\n" " // The name of a persistent TUN device to use.\n" diff --git a/interface/tuntap/AndroidWrapper.c b/interface/tuntap/AndroidWrapper.c new file mode 100644 index 000000000..a2642b16e --- /dev/null +++ b/interface/tuntap/AndroidWrapper.c @@ -0,0 +1,92 @@ +/* vim: set expandtab ts=4 sw=4: */ +/* + * You may redistribute this program and/or modify it under the terms of + * the GNU General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "interface/Iface.h" +#include "interface/tuntap/AndroidWrapper.h" +#include "util/platform/Sockaddr.h" +#include "memory/Allocator.h" +#include "util/Assert.h" +#include "util/Identity.h" +#include "wire/Ethernet.h" +#include "wire/Headers.h" +#include "wire/Message.h" +#include "wire/Error.h" + +/** + * Android VpnService is expect you to read/write packet from the tun device + * file description opened by system process rather than in the cjd process, + * this InterfaceWrapper handle this case. + */ + +struct AndroidWrapper_pvt +{ + struct AndroidWrapper pub; + struct Log* logger; + Identity +}; + +static Iface_DEFUN incomingFromWire(struct Message* msg, struct Iface* externalIf) +{ + struct AndroidWrapper_pvt* ctx = + Identity_containerOf(externalIf, struct AndroidWrapper_pvt, pub.externalIf); + + if (!ctx->pub.internalIf.connectedIf) { + Log_debug(ctx->logger, "DROP message for android tun not inited"); + return NULL; + } + + int version = Headers_getIpVersion(msg->bytes); + uint16_t ethertype = 0; + if (version == 4) { + ethertype = Ethernet_TYPE_IP4; + } else if (version == 6) { + ethertype = Ethernet_TYPE_IP6; + } else { + Log_debug(ctx->logger, "Message is not IP/IPv6, dropped."); + return NULL; + } + + Message_shift(msg, 4, NULL); + ((uint16_t*) msg->bytes)[0] = 0; + ((uint16_t*) msg->bytes)[1] = ethertype; + + return Iface_next(&ctx->pub.internalIf, msg); +} + +static Iface_DEFUN incomingFromUs(struct Message* msg, struct Iface* internalIf) +{ + struct AndroidWrapper_pvt* ctx = + Identity_containerOf(internalIf, struct AndroidWrapper_pvt, pub.internalIf); + + if (!ctx->pub.externalIf.connectedIf) { + Log_debug(ctx->logger, "DROP message for android tun not inited"); + return NULL; + } + + Message_shift(msg, -4, NULL); + + return Iface_next(&ctx->pub.externalIf, msg); +} + +struct AndroidWrapper* AndroidWrapper_new(struct Allocator* alloc, struct Log* log) +{ + struct AndroidWrapper_pvt* context = + Allocator_calloc(alloc, sizeof(struct AndroidWrapper_pvt), 1); + Identity_set(context); + context->pub.externalIf.send = incomingFromWire; + context->pub.internalIf.send = incomingFromUs; + context->logger = log; + + return &context->pub; +} diff --git a/interface/tuntap/AndroidWrapper.h b/interface/tuntap/AndroidWrapper.h new file mode 100644 index 000000000..52d45b3b4 --- /dev/null +++ b/interface/tuntap/AndroidWrapper.h @@ -0,0 +1,32 @@ +/* vim: set expandtab ts=4 sw=4: */ +/* + * You may redistribute this program and/or modify it under the terms of + * the GNU General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef AndroidWrapper_H +#define AndroidWrapper_H + +#include "interface/Iface.h" +#include "memory/Allocator.h" +#include "util/log/Log.h" +#include "util/Linker.h" +Linker_require("interface/tuntap/AndroidWrapper.c"); + +struct AndroidWrapper +{ + struct Iface internalIf; + struct Iface externalIf; +}; + +struct AndroidWrapper* AndroidWrapper_new(struct Allocator* alloc, struct Log* log); + +#endif diff --git a/util/events/FileNo.h b/util/events/FileNo.h new file mode 100644 index 000000000..34472f128 --- /dev/null +++ b/util/events/FileNo.h @@ -0,0 +1,63 @@ +/* vim: set expandtab ts=4 sw=4: */ +/* + * You may redistribute this program and/or modify it under the terms of + * the GNU General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef FileNo_H +#define FileNo_H + +#include "memory/Allocator.h" +#include "exception/Except.h" +#include "util/events/EventBase.h" +#include "util/Linker.h" +#include "util/log/Log.h" +Linker_require("util/events/libuv/FileNo.c"); + +struct FileNo; + +enum FileNo_Type { + /** Normal tunfd type, no need other wrapper */ + FileNo_Type_NORMAL = 0, + + /** Android tunfd, need AndroidWrapper */ + FileNo_Type_ANDROID +}; + +typedef void (* FileNo_callback)(void* context, enum FileNo_Type type, int fileno); + +struct FileNo +{ + /** The name of the file eg: "/tmp/cjdns_fileno_foo" */ + const char* const pipePath; + + void* userData; + + enum FileNo_Type type; + + struct EventBase* const base; + + FileNo_callback onFileNoReceived; + + struct Log* logger; +}; + +#define FileNo_PADDING_AMOUNT 512 +#define FileNo_BUFFER_CAP 4000 + +struct FileNo* FileNo_new(const char* path, + struct EventBase* eb, + struct Except* eh, + struct Log* logger, + struct Allocator* userAlloc, + FileNo_callback recv_cb); + +#endif diff --git a/util/events/libuv/FileNo.c b/util/events/libuv/FileNo.c new file mode 100644 index 000000000..c67a784fa --- /dev/null +++ b/util/events/libuv/FileNo.c @@ -0,0 +1,221 @@ +/* vim: set expandtab ts=4 sw=4: */ +/* + * You may redistribute this program and/or modify it under the terms of + * the GNU General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "util/events/libuv/UvWrapper.h" +#include "benc/String.h" +#include "memory/Allocator.h" +#include "util/events/FileNo.h" +#include "util/events/libuv/EventBase_pvt.h" +#include "util/log/Log.h" +#include "util/Identity.h" + +#include +#include + +struct FileNo_pvt +{ + struct FileNo pub; + + uv_pipe_t server; + uv_pipe_t peer; + + /** Job to close the handles when the allocator is freed */ + struct Allocator_OnFreeJob* closeHandlesOnFree; + + int isActive; + + struct Allocator* alloc; + + Identity +}; + +static void onClose(uv_handle_t* handle) +{ + struct FileNo_pvt* fileno = Identity_check((struct FileNo_pvt*)handle->data); + handle->data = NULL; + if (fileno->closeHandlesOnFree && !fileno->server.data && !fileno->peer.data) { + Allocator_onFreeComplete((struct Allocator_OnFreeJob*) fileno->closeHandlesOnFree); + } +} + +#if FileNo_PADDING_AMOUNT < 8 + #error +#endif +#define ALLOC(buff) (((struct Allocator**) &(buff[-(8 + (((uintptr_t)buff) % 8))]))[0]) + +static void incoming(uv_pipe_t* stream, + ssize_t nread, + const uv_buf_t* buf, + uv_handle_type pending) +{ + struct FileNo_pvt* fileno = Identity_check((struct FileNo_pvt*) stream->data); + + // Grab out the allocator which was placed there by allocate() + struct Allocator* alloc = buf->base ? ALLOC(buf->base) : NULL; + + Assert_true(pending == UV_UNKNOWN_HANDLE); + + if (nread < 0) { + uv_close((uv_handle_t*) stream, onClose); + } else if (fileno->peer.accepted_fd != -1) { + if (fileno->pub.onFileNoReceived) { + fileno->pub.onFileNoReceived(fileno->pub.userData, fileno->pub.type, + fileno->peer.accepted_fd); + } + } + + if (alloc) { + Allocator_free(alloc); + } +} + +static void allocate(uv_handle_t* handle, size_t size, uv_buf_t* buf) +{ + struct FileNo_pvt* pipe = Identity_check((struct FileNo_pvt*) handle->data); + size = FileNo_BUFFER_CAP; + size_t fullSize = size + FileNo_PADDING_AMOUNT; + + struct Allocator* child = Allocator_child(pipe->alloc); + char* buff = Allocator_malloc(child, fullSize); + buff += FileNo_PADDING_AMOUNT; + + ALLOC(buff) = child; + + buf->base = buff; + buf->len = size; +} + +static void connected(uv_connect_t* req, int status) +{ + uv_stream_t* link = req->handle; + struct FileNo_pvt* fileno = Identity_check((struct FileNo_pvt*) link->data); + Log_debug(fileno->pub.logger, "FileNo [%s] established connection", fileno->pub.pipePath); + + int ret; + if (status) { + Log_info(fileno->pub.logger, "uv_pipe_connect() failed for pipe [%s] [%s]", + fileno->pub.pipePath, uv_strerror(status) ); + uv_close((uv_handle_t*) &fileno->peer, onClose); + + } else if ((ret = uv_read2_start((uv_stream_t*)&fileno->peer, allocate, incoming))) { + Log_info(fileno->pub.logger, "uv_read2_start() failed for pipe [%s] [%s]", + fileno->pub.pipePath, uv_strerror(ret)); + uv_close((uv_handle_t*) &fileno->peer, onClose); + + } else { + fileno->isActive = 1; + } +} + +static void listenCallback(uv_stream_t* server, int status) +{ + struct FileNo_pvt* fileno = Identity_check((struct FileNo_pvt*) server->data); + if (fileno->isActive) { + // first connection wins. + return; + } + if (status == -1) { + Log_info(fileno->pub.logger, "failed to accept pipe connection [%s] [%s]", + fileno->pub.pipePath, uv_strerror(status) ); + return; + } + + Log_info(fileno->pub.logger, "Try accept pipe connection [%s]", + fileno->pub.pipePath); + + int ret = uv_accept(server, (uv_stream_t*) &fileno->peer); + if (ret) { + Log_warn(fileno->pub.logger, "uv_accept() failed: pipe [%s] [%s]", + fileno->pub.pipePath, uv_strerror(ret) ); + uv_close((uv_handle_t*) &fileno->peer, onClose); + } else { + uv_connect_t req = { .handle = (uv_stream_t*) &fileno->peer }; + connected(&req, 0); + } +} + +static int closeHandlesOnFree(struct Allocator_OnFreeJob* job) +{ + struct FileNo_pvt* fileno = Identity_check((struct FileNo_pvt*)job->userData); + fileno->closeHandlesOnFree = job; + int skip = 2; + if (fileno->server.data) { + uv_close((uv_handle_t*) &fileno->server, NULL); + skip--; + } + if (fileno->peer.data) { + uv_close((uv_handle_t*) &fileno->peer, NULL); + skip--; + } + if (skip == 2) { + return 0; + } + return Allocator_ONFREE_ASYNC; +} + +struct FileNo* FileNo_new(const char* path, + struct EventBase* eb, + struct Except* eh, + struct Log* logger, + struct Allocator* userAlloc, + FileNo_callback recv_cb) +{ + struct EventBase_pvt* ctx = EventBase_privatize(eb); + struct Allocator* alloc = Allocator_child(userAlloc); + String* pathStr = String_new(path, alloc); + + struct FileNo_pvt* out = Allocator_clone(alloc, (&(struct FileNo_pvt) { + .pub = { + .pipePath = pathStr->bytes, + .base = eb, + .logger = logger, + .onFileNoReceived = recv_cb + }, + .alloc = alloc + })); + + int ret = uv_pipe_init(ctx->loop, &out->peer, 1); + if (ret) { + Except_throw(eh, "uv_pipe_init() failed [%s]", uv_strerror(ret)); + } + + ret = uv_pipe_init(ctx->loop, &out->server, 1); + if (ret) { + Except_throw(eh, "uv_pipe_init() failed [%s]", uv_strerror(ret)); + } + + Allocator_onFree(alloc, closeHandlesOnFree, out); + + out->peer.data = out; + out->server.data = out; + Identity_set(out); + + uv_fs_t req; + uv_fs_unlink(out->peer.loop, &req, out->pub.pipePath, NULL); + + ret = uv_pipe_bind(&out->server, out->pub.pipePath); + if (!ret) { + ret = uv_listen((uv_stream_t*) &out->server, 1, listenCallback); + if (ret) { + Except_throw(eh, "uv_listen() failed [%s] for pipe [%s]", + uv_strerror(ret), out->pub.pipePath); + } + return &out->pub; + } + + Except_throw(eh, "uv_pipe_bind() failed [%s] for pipe [%s]", + uv_strerror(ret), out->pub.pipePath); + + return &out->pub; +} diff --git a/util/events/libuv/FileNo_admin.c b/util/events/libuv/FileNo_admin.c new file mode 100644 index 000000000..4f0829eb3 --- /dev/null +++ b/util/events/libuv/FileNo_admin.c @@ -0,0 +1,92 @@ +/* vim: set expandtab ts=4 sw=4: */ +/* + * You may redistribute this program and/or modify it under the terms of + * the GNU General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "admin/Admin.h" +#include "benc/Dict.h" +#include "benc/List.h" +#include "benc/String.h" +#include "util/log/Log.h" +#include "util/Identity.h" +#include "util/events/FileNo.h" +#include "util/events/libuv/FileNo_admin.h" +#include "util/events/Time.h" +#include "util/events/Timeout.h" + +struct Context +{ + struct FileNo_admin pub; + struct Admin* admin; + struct Allocator* alloc; + struct FileNo* fileno; + struct Except* eh; + struct EventBase* base; + struct Log* logger; + FileNo_callback onFileNoReceived; + Identity +}; + +static void import(Dict* args, void* vcontext, String* txid, struct Allocator* requestAlloc) +{ + struct Context* ctx = Identity_check((struct Context*) vcontext); + String* path = Dict_getString(args, String_CONST("path")); + String* type = Dict_getString(args, String_CONST("type")); + char* error = NULL; + + if (ctx->fileno && CString_strncmp(ctx->fileno->pipePath, path->bytes, path->len)) { + error = "none"; + } else if (ctx->fileno) { + error = "FileNo already exist"; + } else { + int fdtype = FileNo_Type_NORMAL; + if (type && CString_strcmp(type->bytes, "android")) { + fdtype = FileNo_Type_ANDROID; + } + ctx->fileno = FileNo_new(path->bytes, ctx->base, ctx->eh, ctx->logger, ctx->alloc, + ctx->onFileNoReceived); + ctx->fileno->type = fdtype; + ctx->fileno->userData = ctx->pub.userData; + error = "none"; + } + + Dict response = Dict_CONST( + String_CONST("error"), String_OBJ(String_CONST(error)), NULL + ); + Admin_sendMessage(&response, txid, ctx->admin); +} + +struct FileNo_admin* FileNo_admin_new(struct Admin* admin, + struct Allocator* alloc, + struct EventBase* base, + struct Log* logger, + struct Except* eh, + FileNo_callback cb) +{ + struct Context* ctx = Allocator_clone(alloc, (&(struct Context) { + .admin = admin, + .eh = eh, + .alloc = alloc, + .logger = logger, + .onFileNoReceived = cb, + .base = base + })); + Identity_set(ctx); + + Admin_registerFunction("FileNo_import", import, ctx, true, + ((struct Admin_FunctionArg[]) { + { .name = "path", .required = 1, .type = "String" }, + { .name = "type", .required = 0, .type = "String" } + }), admin); + + return &ctx->pub; +} diff --git a/util/events/libuv/FileNo_admin.h b/util/events/libuv/FileNo_admin.h new file mode 100644 index 000000000..6388a802d --- /dev/null +++ b/util/events/libuv/FileNo_admin.h @@ -0,0 +1,38 @@ +/* vim: set expandtab ts=4 sw=4: */ +/* + * You may redistribute this program and/or modify it under the terms of + * the GNU General Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef FileNo_admin_H +#define FileNo_admin_H + +#include "admin/Admin.h" +#include "memory/Allocator.h" +#include "util/log/Log.h" +#include "util/events/EventBase.h" +#include "util/events/FileNo.h" +#include "util/Linker.h" +Linker_require("util/events/libuv/FileNo_admin.c"); + +struct FileNo_admin +{ + void* userData; +}; + +struct FileNo_admin* FileNo_admin_new(struct Admin* admin, + struct Allocator* alloc, + struct EventBase* base, + struct Log* logger, + struct Except* eh, + FileNo_callback cb); + +#endif