diff options
author | Adam <adam@sigterm.info> | 2014-02-17 22:21:08 -0500 |
---|---|---|
committer | Adam <adam@sigterm.info> | 2014-02-17 22:21:08 -0500 |
commit | 0db81ca46ad3a9faeacd9b138ba4c1634cd1faab (patch) | |
tree | f3c7d3da8a0f8ae30d6c8b867bbf8f594793b256 | |
parent | 7f7144de1fe96b42e3845690b77738e7b93bbe46 (diff) | |
parent | 1c39d25ccaabeab2a8b1f317bf5394f007db14f8 (diff) |
Merge pull request #52 from attilamolnar/2.0+gnutls
Add support for SSL connections powered by GnuTLS
-rw-r--r-- | data/example.conf | 2 | ||||
-rw-r--r-- | data/modules.example.conf | 86 | ||||
-rw-r--r-- | data/stats.standalone.example.conf | 2 | ||||
-rw-r--r-- | docs/Changes.conf | 1 | ||||
-rw-r--r-- | docs/WIN32.txt | 7 | ||||
-rw-r--r-- | include/service.h | 2 | ||||
-rw-r--r-- | modules/extra/m_ssl_gnutls.cpp | 509 | ||||
-rw-r--r-- | modules/extra/m_ssl_openssl.cpp (renamed from modules/extra/m_ssl.cpp) | 4 | ||||
-rw-r--r-- | modules/m_httpd.cpp | 3 |
9 files changed, 583 insertions, 33 deletions
diff --git a/data/example.conf b/data/example.conf index fbb7e310e..4d4ba6b17 100644 --- a/data/example.conf +++ b/data/example.conf @@ -168,7 +168,7 @@ uplink /* * Enable if Services should connect using SSL. - * You must have m_ssl loaded for this to work. + * You must have an SSL module loaded for this to work. */ ssl = no diff --git a/data/modules.example.conf b/data/modules.example.conf index e58ebea88..8fe9ca4db 100644 --- a/data/modules.example.conf +++ b/data/modules.example.conf @@ -201,7 +201,7 @@ module { name = "help" } /* Time before connections to this server are timed out. */ timeout = 30 - /* Listen using SSL. Requires m_ssl. */ + /* Listen using SSL. Requires an SSL module. */ #ssl = yes /* If you are using a reverse proxy that sends one of the @@ -469,6 +469,69 @@ module { name = "help" } #module { name = "m_sasl_dh-blowfish" } /* + * m_ssl_gnutls [EXTRA] + * + * This module provides SSL services to Anope using GnuTLS, for example to + * connect to the uplink server(s) via SSL. + * + * You may only load either m_ssl_gnutls or m_ssl_openssl, bot not both. + */ +#module +{ + name = "m_ssl_gnutls" + + /* + * An optional certificate and key for m_gnutls to give to the uplink. + * + * You can generate your own certificate and key pair by using: + * + * certtool --generate-privkey --bits 2048 --outfile anope.key + * certtool --generate-self-signed --load-privkey anope.key --outfile anope.crt + * + */ + cert = "data/anope.crt" + key = "data/anope.key" + + /* + * Diffie-Hellman parameters to use when acting as a server. This is only + * required for TLS servers that want to use ephemeral DH cipher suites. + * + * This is NOT required for Anope to connect to the uplink server(s) via SSL. + * + * You can generate DH parameters by using: + * + * certtool --generate-dh-params --bits 2048 --outfile dhparams.pem + * + */ +# dhparams = "data/dhparams.pem" +} + +/* + * m_ssl_openssl [EXTRA] + * + * This module provides SSL services to Anope using OpenSSL, for example to + * connect to the uplink server(s) via SSL. + * + * You may only load either m_ssl_openssl or m_ssl_gnutls, bot not both. + * + */ +#module +{ + name = "m_ssl_openssl" + + /* + * An optional certificate and key for m_openssl to give to the uplink. + * + * You can generate your own certificate and key pair by using: + * + * openssl genrsa -out anope.key 2048 + * openssl req -new -x509 -key anope.key -out anope.crt -days 1095 + */ + cert = "data/anope.crt" + key = "data/anope.key" +} + +/* * m_sql_authentication [EXTRA] * * This module allows authenticating users against an external SQL database using a custom @@ -663,27 +726,6 @@ module { name = "help" } } /* - * m_ssl [EXTRA] - * - * This module uses SSL to connect to the uplink server(s). - */ -#module -{ - name = "m_ssl" - - /* - * An optional certificate and key for m_ssl to give to the uplink. - * - * You can generate your own certificate and key pair by using: - * - * openssl genrsa -out anope.key 2048 - * openssl req -new -x509 -key anope.key -out anope.crt -days 1095 - */ - cert = "data/anope.crt" - key = "data/anope.key" -} - -/* * m_xmlrpc * * Allows remote applications (websites) to execute queries in real time to retrieve data from Anope. diff --git a/data/stats.standalone.example.conf b/data/stats.standalone.example.conf index 7ba985e19..8db7c9999 100644 --- a/data/stats.standalone.example.conf +++ b/data/stats.standalone.example.conf @@ -168,7 +168,7 @@ uplink /* * Enable if Services should connect using SSL. - * You must have m_ssl loaded for this to work. + * You must have an SSL module loaded for this to work. */ ssl = no diff --git a/docs/Changes.conf b/docs/Changes.conf index aae151fd9..8b41ad2be 100644 --- a/docs/Changes.conf +++ b/docs/Changes.conf @@ -2,6 +2,7 @@ Anope Version 2.0.0 ------------------- options:passlen, enforceruser, enforcerhost, releasetimeout, and guestnickprefix moved to nickserv's module configuration options:hideregisteredcommands added +m_ssl renamed to m_ssl_openssl Anope Version 1.9.9 ------------------- diff --git a/docs/WIN32.txt b/docs/WIN32.txt index 7922523f4..018b898d2 100644 --- a/docs/WIN32.txt +++ b/docs/WIN32.txt @@ -71,9 +71,10 @@ Anope for Windows our IRC Support channel for assistance.
Some Anope modules require third party libraries, such as m_mysql and
- m_ssl. If these libraries are installed in nonstandard locations, cmake
- will probably not find them and should be told where they are by passing
- additional search paths to the last question in Config, such as:
+ the SSL modules. If these libraries are installed in nonstandard
+ locations, cmake will probably not find them and should be told where
+ they are by passing additional search paths to the last question in
+ Config, such as:
-DEXTRA_INCLUDE:STRING=c:/openssl/include;c:/mysql/include
-DEXTRA_LIBS:STRING=c:/openssl/lib;c:/mysql/lib
diff --git a/include/service.h b/include/service.h index db0b2af4b..8142363d6 100644 --- a/include/service.h +++ b/include/service.h @@ -20,7 +20,7 @@ /** Anything that inherits from this class can be referred to * using ServiceReference. Any interfaces provided by modules, * such as commands, use this. This is also used for modules - * that publish a service (m_ssl, etc). + * that publish a service (m_ssl_openssl, etc). */ class CoreExport Service : public virtual Base { diff --git a/modules/extra/m_ssl_gnutls.cpp b/modules/extra/m_ssl_gnutls.cpp new file mode 100644 index 000000000..002015f5f --- /dev/null +++ b/modules/extra/m_ssl_gnutls.cpp @@ -0,0 +1,509 @@ +/* + * (C) 2014 Anope Team + * Contact us at team@anope.org + * + * Please read COPYING and README for further details. + */ + +/* RequiredLibraries: gnutls */ + +#include "module.h" +#include "modules/ssl.h" + +#include <errno.h> +#include <gnutls/gnutls.h> + +class GnuTLSModule; +static GnuTLSModule *me; + +class MySSLService : public SSLService +{ + public: + MySSLService(Module *o, const Anope::string &n); + + /** Initialize a socket to use SSL + * @param s The socket + */ + void Init(Socket *s) anope_override; +}; + +class SSLSocketIO : public SocketIO +{ + public: + gnutls_session_t sess; + + /** Constructor + */ + SSLSocketIO() : sess(NULL) { } + + /** Really receive something from the buffer + * @param s The socket + * @param buf The buf to read to + * @param sz How much to read + * @return Number of bytes received + */ + int Recv(Socket *s, char *buf, size_t sz) anope_override; + + /** Write something to the socket + * @param s The socket + * @param buf The data to write + * @param size The length of the data + */ + int Send(Socket *s, const char *buf, size_t sz) anope_override; + + /** Accept a connection from a socket + * @param s The socket + * @return The new socket + */ + ClientSocket *Accept(ListenSocket *s) anope_override; + + /** Finished accepting a connection from a socket + * @param s The socket + * @return SF_ACCEPTED if accepted, SF_ACCEPTING if still in process, SF_DEAD on error + */ + SocketFlag FinishAccept(ClientSocket *cs) anope_override; + + /** Connect the socket + * @param s THe socket + * @param target IP to connect to + * @param port to connect to + */ + void Connect(ConnectionSocket *s, const Anope::string &target, int port) anope_override; + + /** Called to potentially finish a pending connection + * @param s The socket + * @return SF_CONNECTED on success, SF_CONNECTING if still pending, and SF_DEAD on error. + */ + SocketFlag FinishConnect(ConnectionSocket *s) anope_override; + + /** Called when the socket is destructing + */ + void Destroy() anope_override; +}; + +namespace GnuTLS +{ + class Init + { + public: + Init() { gnutls_global_init(); } + ~Init() { gnutls_global_deinit(); } + }; + + /** Used to create a gnutls_datum_t* from an Anope::string + */ + class Datum + { + gnutls_datum_t datum; + + public: + Datum(const Anope::string &dat) + { + datum.data = reinterpret_cast<unsigned char *>(const_cast<char *>(dat.data())); + datum.size = static_cast<unsigned int>(dat.length()); + } + + const gnutls_datum_t *get() const { return &datum; } + }; + + class DHParams + { + gnutls_dh_params_t dh_params; + + public: + DHParams() : dh_params(NULL) { } + + void Import(const Anope::string &dhstr) + { + if (dh_params != NULL) + { + gnutls_dh_params_deinit(dh_params); + dh_params = NULL; + } + + int ret = gnutls_dh_params_init(&dh_params); + if (ret < 0) + throw ConfigException("Unable to initialize DH parameters"); + + ret = gnutls_dh_params_import_pkcs3(dh_params, Datum(dhstr).get(), GNUTLS_X509_FMT_PEM); + if (ret < 0) + { + gnutls_dh_params_deinit(dh_params); + dh_params = NULL; + throw ConfigException("Unable to import DH parameters"); + } + } + + ~DHParams() + { + if (dh_params) + gnutls_dh_params_deinit(dh_params); + } + + gnutls_dh_params_t get() const { return dh_params; } + }; + + class X509CertCredentials + { + gnutls_certificate_credentials_t cred; + DHParams dh; + + public: + X509CertCredentials() + { + if (gnutls_certificate_allocate_credentials(&cred) < 0) + throw ConfigException("Cannot allocate certificate credentials"); + } + + ~X509CertCredentials() + { + gnutls_certificate_free_credentials(cred); + } + + void SetupSession(gnutls_session_t sess) + { + gnutls_credentials_set(sess, GNUTLS_CRD_CERTIFICATE, cred); + gnutls_set_default_priority(sess); + } + + void SetCertAndKey(const Anope::string &certfile, const Anope::string &keyfile) + { + int ret = gnutls_certificate_set_x509_key_file(cred, certfile.c_str(), keyfile.c_str(), GNUTLS_X509_FMT_PEM); + if (ret < 0) + throw ConfigException("Unable to load certificate/private key: " + Anope::string(gnutls_strerror(ret))); + } + + void SetDH(const Anope::string &dhfile) + { + std::ifstream ifs(dhfile.c_str()); + const Anope::string dhdata((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>()); + + dh.Import(dhdata); + gnutls_certificate_set_dh_params(cred, dh.get()); + } + + bool HasDH() const + { + return (dh.get() != NULL); + } + }; +} + +class GnuTLSModule : public Module +{ + GnuTLS::Init libinit; + + public: + GnuTLS::X509CertCredentials cred; + MySSLService service; + + GnuTLSModule(const Anope::string &modname, const Anope::string &creator) : Module(modname, creator, EXTRA | VENDOR), service(this, "ssl") + { + me = this; + this->SetPermanent(true); + } + + ~GnuTLSModule() + { + for (std::map<int, Socket *>::const_iterator it = SocketEngine::Sockets.begin(), it_end = SocketEngine::Sockets.end(); it != it_end;) + { + Socket *s = it->second; + ++it; + + if (dynamic_cast<SSLSocketIO *>(s->io)) + delete s; + } + } + + static void CheckFile(const Anope::string &filename) + { + if (!Anope::IsFile(filename.c_str())) + { + Log() << "File does not exist: " << filename; + throw ConfigException("Error loading certificate/private key"); + } + } + + void OnReload(Configuration::Conf *conf) anope_override + { + Configuration::Block *config = conf->GetModule(this); + + const Anope::string certfile = config->Get<const Anope::string>("cert", "data/anope.crt"); + const Anope::string keyfile = config->Get<const Anope::string>("key", "data/anope.key"); + const Anope::string dhfile = config->Get<const Anope::string>("dh", "data/dhparams.pem"); + + CheckFile(certfile); + CheckFile(keyfile); + + // DH params is not mandatory + if (Anope::IsFile(dhfile.c_str())) + { + cred.SetDH(dhfile); + Log(LOG_DEBUG) << "m_ssl_gnutls: Successfully loaded DH parameters from " << dhfile; + } + + cred.SetCertAndKey(certfile, keyfile); + Log(LOG_DEBUG) << "m_ssl_gnutls: Successfully loaded certificate " << certfile << " and private key " << keyfile; + } + + void OnPreServerConnect() anope_override + { + Configuration::Block *config = Config->GetBlock("uplink", Anope::CurrentUplink); + + if (config->Get<bool>("ssl")) + { + this->service.Init(UplinkSock); + } + } +}; + +MySSLService::MySSLService(Module *o, const Anope::string &n) : SSLService(o, n) +{ +} + +void MySSLService::Init(Socket *s) +{ + if (s->io != &NormalSocketIO) + throw CoreException("Socket initializing SSL twice"); + + s->io = new SSLSocketIO(); +} + +int SSLSocketIO::Recv(Socket *s, char *buf, size_t sz) +{ + int ret = gnutls_record_recv(this->sess, buf, sz); + + if (ret > 0) + TotalRead += ret; + else if (ret < 0) + { + switch (ret) + { + case GNUTLS_E_AGAIN: + case GNUTLS_E_INTERRUPTED: + SocketEngine::SetLastError(EAGAIN); + break; + default: + if (s == UplinkSock) + { + // Log and fake an errno because this is a fatal error on the uplink socket + Log() << "SSL error: " << gnutls_strerror(ret); + } + SocketEngine::SetLastError(ECONNRESET); + } + } + + return ret; +} + +int SSLSocketIO::Send(Socket *s, const char *buf, size_t sz) +{ + int ret = gnutls_record_send(this->sess, buf, sz); + + if (ret > 0) + TotalWritten += ret; + else + { + switch (ret) + { + case 0: + case GNUTLS_E_AGAIN: + case GNUTLS_E_INTERRUPTED: + SocketEngine::SetLastError(EAGAIN); + break; + default: + if (s == UplinkSock) + { + // Log and fake an errno because this is a fatal error on the uplink socket + Log() << "SSL error: " << gnutls_strerror(ret); + } + SocketEngine::SetLastError(ECONNRESET); + } + } + + return ret; +} + +ClientSocket *SSLSocketIO::Accept(ListenSocket *s) +{ + if (s->io == &NormalSocketIO) + throw SocketException("Attempting to accept on uninitialized socket with SSL"); + + sockaddrs conaddr; + + socklen_t size = sizeof(conaddr); + int newsock = accept(s->GetFD(), &conaddr.sa, &size); + +#ifndef INVALID_SOCKET + const int INVALID_SOCKET = -1; +#endif + + if (newsock < 0 || newsock == INVALID_SOCKET) + throw SocketException("Unable to accept connection: " + Anope::LastError()); + + ClientSocket *newsocket = s->OnAccept(newsock, conaddr); + me->service.Init(newsocket); + SSLSocketIO *io = anope_dynamic_static_cast<SSLSocketIO *>(newsocket->io); + + if (gnutls_init(&io->sess, GNUTLS_SERVER) != GNUTLS_E_SUCCESS) + throw SocketException("Unable to initialize SSL socket"); + + me->cred.SetupSession(io->sess); + gnutls_transport_set_int(io->sess, newsock); + + newsocket->flags[SF_ACCEPTING] = true; + this->FinishAccept(newsocket); + + return newsocket; +} + +SocketFlag SSLSocketIO::FinishAccept(ClientSocket *cs) +{ + if (cs->io == &NormalSocketIO) + throw SocketException("Attempting to finish connect uninitialized socket with SSL"); + else if (cs->flags[SF_ACCEPTED]) + return SF_ACCEPTED; + else if (!cs->flags[SF_ACCEPTING]) + throw SocketException("SSLSocketIO::FinishAccept called for a socket not accepted nor accepting?"); + + SSLSocketIO *io = anope_dynamic_static_cast<SSLSocketIO *>(cs->io); + + int ret = gnutls_handshake(io->sess); + if (ret < 0) + { + if (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED) + { + // gnutls_handshake() wants to read or write again; + // if gnutls_record_get_direction() returns 0 it wants to read, otherwise it wants to write. + if (gnutls_record_get_direction(io->sess) == 0) + { + SocketEngine::Change(cs, false, SF_WRITABLE); + SocketEngine::Change(cs, true, SF_READABLE); + } + else + { + SocketEngine::Change(cs, true, SF_WRITABLE); + SocketEngine::Change(cs, false, SF_READABLE); + } + return SF_ACCEPTING; + } + else + { + cs->OnError(Anope::string(gnutls_strerror(ret))); + cs->flags[SF_DEAD] = true; + cs->flags[SF_ACCEPTING] = false; + return SF_DEAD; + } + } + else + { + cs->flags[SF_ACCEPTED] = true; + cs->flags[SF_ACCEPTING] = false; + SocketEngine::Change(cs, false, SF_WRITABLE); + SocketEngine::Change(cs, true, SF_READABLE); + cs->OnAccept(); + return SF_ACCEPTED; + } +} + +void SSLSocketIO::Connect(ConnectionSocket *s, const Anope::string &target, int port) +{ + if (s->io == &NormalSocketIO) + throw SocketException("Attempting to connect uninitialized socket with SSL"); + + s->flags[SF_CONNECTING] = s->flags[SF_CONNECTED] = false; + + s->conaddr.pton(s->IsIPv6() ? AF_INET6 : AF_INET, target, port); + int c = connect(s->GetFD(), &s->conaddr.sa, s->conaddr.size()); + if (c == -1) + { + if (Anope::LastErrorCode() != EINPROGRESS) + { + s->OnError(Anope::LastError()); + s->flags[SF_DEAD] = true; + return; + } + else + { + SocketEngine::Change(s, true, SF_WRITABLE); + s->flags[SF_CONNECTING] = true; + return; + } + } + else + { + s->flags[SF_CONNECTING] = true; + this->FinishConnect(s); + } +} + +SocketFlag SSLSocketIO::FinishConnect(ConnectionSocket *s) +{ + if (s->io == &NormalSocketIO) + throw SocketException("Attempting to finish connect uninitialized socket with SSL"); + else if (s->flags[SF_CONNECTED]) + return SF_CONNECTED; + else if (!s->flags[SF_CONNECTING]) + throw SocketException("SSLSocketIO::FinishConnect called for a socket not connected nor connecting?"); + + SSLSocketIO *io = anope_dynamic_static_cast<SSLSocketIO *>(s->io); + + if (io->sess == NULL) + { + if (gnutls_init(&io->sess, GNUTLS_CLIENT) != GNUTLS_E_SUCCESS) + throw SocketException("Unable to initialize SSL socket"); + me->cred.SetupSession(io->sess); + gnutls_transport_set_int(io->sess, s->GetFD()); + } + + int ret = gnutls_handshake(io->sess); + if (ret < 0) + { + if (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED) + { + // gnutls_handshake() wants to read or write again; + // if gnutls_record_get_direction() returns 0 it wants to read, otherwise it wants to write. + if (gnutls_record_get_direction(io->sess) == 0) + { + SocketEngine::Change(s, false, SF_WRITABLE); + SocketEngine::Change(s, true, SF_READABLE); + } + else + { + SocketEngine::Change(s, true, SF_WRITABLE); + SocketEngine::Change(s, false, SF_READABLE); + } + + return SF_CONNECTING; + } + else + { + s->OnError(Anope::string(gnutls_strerror(ret))); + s->flags[SF_CONNECTING] = false; + s->flags[SF_DEAD] = true; + return SF_DEAD; + } + } + else + { + s->flags[SF_CONNECTING] = false; + s->flags[SF_CONNECTED] = true; + SocketEngine::Change(s, false, SF_WRITABLE); + SocketEngine::Change(s, true, SF_READABLE); + s->OnConnect(); + return SF_CONNECTED; + } +} + +void SSLSocketIO::Destroy() +{ + if (this->sess) + { + gnutls_bye(this->sess, GNUTLS_SHUT_WR); + gnutls_deinit(this->sess); + } + + delete this; +} + +MODULE_INIT(GnuTLSModule) diff --git a/modules/extra/m_ssl.cpp b/modules/extra/m_ssl_openssl.cpp index 44971908c..a274dc78d 100644 --- a/modules/extra/m_ssl.cpp +++ b/modules/extra/m_ssl_openssl.cpp @@ -137,7 +137,7 @@ class SSLModule : public Module if (!SSL_CTX_use_certificate_file(client_ctx, this->certfile.c_str(), SSL_FILETYPE_PEM) || !SSL_CTX_use_certificate_file(server_ctx, this->certfile.c_str(), SSL_FILETYPE_PEM)) throw ConfigException("Error loading certificate"); else - Log(LOG_DEBUG) << "m_ssl: Successfully loaded certificate " << this->certfile; + Log(LOG_DEBUG) << "m_ssl_openssl: Successfully loaded certificate " << this->certfile; } else Log() << "Unable to open certificate " << this->certfile; @@ -147,7 +147,7 @@ class SSLModule : public Module if (!SSL_CTX_use_PrivateKey_file(client_ctx, this->keyfile.c_str(), SSL_FILETYPE_PEM) || !SSL_CTX_use_PrivateKey_file(server_ctx, this->keyfile.c_str(), SSL_FILETYPE_PEM)) throw ConfigException("Error loading private key"); else - Log(LOG_DEBUG) << "m_ssl: Successfully loaded private key " << this->keyfile; + Log(LOG_DEBUG) << "m_ssl_openssl: Successfully loaded private key " << this->keyfile; } else { diff --git a/modules/m_httpd.cpp b/modules/m_httpd.cpp index e08452d81..f3d0677a9 100644 --- a/modules/m_httpd.cpp +++ b/modules/m_httpd.cpp @@ -454,9 +454,6 @@ class HTTPD : public Module void OnModuleLoad(User *u, Module *m) anope_override { - if (m->name != "m_ssl") - return; - for (std::map<Anope::string, MyHTTPProvider *>::iterator it = this->providers.begin(), it_end = this->providers.end(); it != it_end; ++it) { MyHTTPProvider *p = it->second; |