diff --git a/CMakeLists.txt b/CMakeLists.txt index d12de2908..3d166bf11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,6 +160,8 @@ install(FILES ${CMAKE_SOURCE_DIR}/COPYRIGHT DESTINATION ${MAXSCALE_SHAREDIR}) install(FILES ${CMAKE_SOURCE_DIR}/README DESTINATION ${MAXSCALE_SHAREDIR}) install(FILES ${CMAKE_SOURCE_DIR}/LICENSE DESTINATION ${MAXSCALE_SHAREDIR}) install(FILES etc/lsyncd_example.conf DESTINATION ${MAXSCALE_SHAREDIR}) +install(FILES Documentation/maxscale.1 DESTINATION ${CMAKE_INSTALL_DATADIR}/man/man1) + # Install startup scripts and ldconfig files if(WITH_SCRIPTS) @@ -206,6 +208,7 @@ if(PACKAGE) else() # Generic CPack configuration variables + set(CPACK_STRIP_FILES FALSE) set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "MaxScale") set(CPACK_PACKAGE_VERSION_MAJOR "${MAXSCALE_VERSION_MAJOR}") set(CPACK_PACKAGE_VERSION_MINOR "${MAXSCALE_VERSION_MINOR}") diff --git a/Documentation/Getting-Started/Building-MaxScale-from-Source-Code.md b/Documentation/Getting-Started/Building-MaxScale-from-Source-Code.md index f83a6e3ae..95236ed7e 100644 --- a/Documentation/Getting-Started/Building-MaxScale-from-Source-Code.md +++ b/Documentation/Getting-Started/Building-MaxScale-from-Source-Code.md @@ -28,7 +28,7 @@ You will need to install all of the following packages for all versions of RHEL, ``` gcc gcc-c++ ncurses-devel bison glibc-devel cmake libgcc perl make libtool -openssl-devel libaio libaio-devel librabbitmq-devel +openssl-devel libaio libaio-devel librabbitmq-devel libcurl-devel pcre-devel ``` In addition, if you wish to to build an RPM package include: @@ -68,7 +68,7 @@ These packages are required on all versions of Ubuntu and Debian. ``` build-essential libssl-dev libaio-dev ncurses-dev bison - cmake perl libtool librabbitmq-dev + cmake perl libtool librabbitmq-dev libcurl-dev libpcre3-dev ``` If you want to build a DEB package, you will also need: diff --git a/Documentation/Getting-Started/Configuration-Guide.md b/Documentation/Getting-Started/Configuration-Guide.md index 15293d1aa..113e28c60 100644 --- a/Documentation/Getting-Started/Configuration-Guide.md +++ b/Documentation/Getting-Started/Configuration-Guide.md @@ -326,6 +326,63 @@ Example: connection_timeout=300 ``` +### Service and SSL + +This section describes configuration parameters for services that control the SSL/TLS encrption method and the various certificate files involved in it. To enable SSL, you must configure the `ssl` parameter with either `enabled` or `required` and provide the three files for `ssl_cert`, `ssl_key` and `ssl_ca_cert`. After this, MySQL connections to this service can be encrypted with SSL. + +#### `ssl` + +This enables SSL connections to the service. If this parameter is set to either `required` or `enabled` and the three certificate files can be found (these are explained afterwards), then client connections will be encrypted with SSL. If the parameter is `enabled` then both SSL and non-SSL connections can connect to this service. If the parameter is set to `required` then only SSL connections can be used for this service and non-SSL connections will get an error when they try to connect to the service. + +#### `ssl_key` + +The SSL private key the service should use. This will be the private key that is used as the server side private key during a client-server SSL handshake. This is a required parameter for SSL enabled services. + +#### `ssl_cert` + +The SSL certificate the service should use. This will be the public certificate that is used as the server side certificate during a client-server SSL handshake. This is a required parameter for SSL enabled services. + +#### `ssl_ca_cert` + +This is the Certificate Authority file. It will be used to verify that both the client and the server certificates are valid. This is a required parameter for SSL enabled services. + +### `ssl_version` + +This parameter controls the level of encryption used. Accepted values are: + * SSLv2 + * SSLv3 + * TLSv10 + * TLSv11 + * TLSv12 + * MAX + +### `ssl_cert_verification_depth` + +The maximum length of the certificate authority chain that will be accepted. Accepted values are positive integers. + +``` +# Example +ssl_cert_verification_depth=10 +``` + +Example SSL enabled service configuration: + +``` +[ReadWriteSplitService] +type=service +router=readwritesplit +servers=server1,server2,server3 +user=myuser +passwd=mypasswd +ssl=required +ssl_cert=/home/markus/certs/server-cert.pem +ssl_key=/home/markus/certs/server-key.pem +ssl_ca_cert=/home/markus/certs/ca.pem +ssl_version=TLSv12 +``` + +This configuration requires all connections to be encryped with SSL. It also specifies that TLSv1.2 should be used as the encryption method. The paths to the server certificate files and the Certificate Authority file are also provided. + ### Server Server sections are used to define the backend database servers that can be formed into a service. A server may be a member of one or more services within MaxScale. Servers are identified by a server name which is the section name in the configuration file. Servers have a type parameter of server, plus address port and protocol parameters. diff --git a/Documentation/Reference/MaxScale-and-SSL.md b/Documentation/Reference/MaxScale-and-SSL.md new file mode 100644 index 000000000..d03a5af52 --- /dev/null +++ b/Documentation/Reference/MaxScale-and-SSL.md @@ -0,0 +1,15 @@ +# MaxScale and SSL + +MaxScale supports client side SSL connections. Enabling is done on a per service basis and each service has its own set of certificates. + +## SSL Options + +Here are the options which relate to SSL and certificates. +Parameter|Values |Description +---------|-----------|-------- +ssl | disabled, enabled, required |`disable` disables SSL, `enabled` enables SSL for client connections but still allows non-SSL connections and `required` requires SSL from all client connections. With the `required` option, client connections that do not use SSL will be rejected. +ssl_cert | path to file |Path to server certificate +ssl_key | path to file |Path to server private key +ssl_ca_cert | path to file |Path to Certificate Authority file +ssl_version|SSLV2,SSLV3,TLSV10,TLSV11,TLSV12,MAX| The SSL method level, defaults to highest available encryption level which is TLSv1.2 +ssl_cert_verify_depth|integer|Certificate authority certificate verification depth, default is 100. diff --git a/Documentation/maxscale.1 b/Documentation/maxscale.1 new file mode 100644 index 000000000..104eaa235 --- /dev/null +++ b/Documentation/maxscale.1 @@ -0,0 +1,69 @@ +.TH maxscale 1 +.SH NAME +maxscale - The intelligent proxy +.SH SYNOPSIS +.B maxscale +[\fIOPTIONS...\fR] +.SH DESCRIPTION +The MariaDB Corporation MaxScale is an intelligent proxy that allows forwarding of +database statements to one or more database servers using complex rules, +a semantic understanding of the database statements and the roles of +the various servers within the backend cluster of databases. + +MaxScale is designed to provide load balancing and high availability +functionality transparently to the applications. In addition it provides +a highly scalable and flexible architecture, with plugin components to +support different protocols and routing decisions. + +.SH OPTIONS +.TP +.BR "-d, --nodaemon" +Run MaxScale in the terminal process +.TP +.BR -f " \fIFILE\fB, --config=\fIFILE\fR" +Relative or absolute pathname of MaxScale configuration file to load. +.TP +.BR -l "[\fIfile|shm\fB], --log=[\fIfile|shm\fB]" +Log trace and debug logs to file or shared memory. The debug and trace logs are disabled by default and if enabled, will log to shared memory. +.TP +.BR -L " \fIPATH\fB, --logdir=\fIPATH\fB" +Path to log file directory. +.TP +.BR -D " \fIPATH\fB, --datadir=\fIPATH\fB" +Path to data directory. This is where the embedded mysql tables are stored in addition to other MaxScale specific data. +.TP +.BR -C " \fIPATH\fB, --configdir=\fIPATH\fB" +Path to configuration file directory. MaxScale will look for the \fImaxscale.cnf\fR file from this folder. +.TP +.BR -B " \fIPATH\fB, --libdir=\fIPATH\fB" +Path to module directory. Modules are only searched from this folder. +.TP +.BR -A " \fIPATH\fB, --cachedir=\fIPATH\fB" +Path to cache directory. This is where MaxScale stores cached authentication data. +.TP +.BR -P " \fIPATH\fB, --piddir=\fIPATH\fB" +Location of MaxScale's PID file. +.TP +.BR -U " \fIUSER\fB, --user=\fIUSER\fB" +Run MaxScale as another user. The user ID and group ID of this user are used to run MaxScale. +.TP +.BR -s " [\fIyes\fB|\fIno\fB], --syslog=[\fIyes\fB|\fIno\fB]" +Log messages to syslog. +.TP +.BR -S " [\fIyes\fB|\fIno\fB], \fB--maxscalelog=[\fIyes\fB|\fIno\fB]" +Log messages to MaxScale's own log files. +.TP +.BR "-v, --version" +Print version information and exit. +.TP +.BR "-?, --help" +Show the help information for MaxScale and exit. + +.SH EXAMPLES +Tutorials on GitHub: +.UR https://github.com/mariadb-corporation/MaxScale/blob/master/Documentation/Documentation-Contents.md#tutorials +.UE +.SH SEE ALSO +The MaxScale documentation on GitHub: +.UR https://github.com/mariadb-corporation/MaxScale/blob/master/Documentation/Documentation-Contents.md +.UE diff --git a/etc/postinst.in b/etc/postinst.in index abf2db1ef..e1d3dc8ad 100755 --- a/etc/postinst.in +++ b/etc/postinst.in @@ -2,11 +2,12 @@ # Create directories -mkdir -p @MAXSCALE_LIBDIR@ -mkdir -p @MAXSCALE_BINDIR@ -mkdir -p @MAXSCALE_SHAREDIR@ -mkdir -p @MAXSCALE_DOCDIR@ -mkdir -p @MAXSCALE_CONFDIR@ +mkdir -p @CMAKE_INSTALL_PREFIX@/@MAXSCALE_LIBDIR@ +mkdir -p @CMAKE_INSTALL_PREFIX@/@MAXSCALE_BINDIR@ +mkdir -p @CMAKE_INSTALL_PREFIX@/@MAXSCALE_SHAREDIR@ +mkdir -p @CMAKE_INSTALL_PREFIX@/@MAXSCALE_DOCDIR@ + +# MAXSCALE_VARDIR is an absolute path to /var by default mkdir -p @MAXSCALE_VARDIR@/log/maxscale mkdir -p @MAXSCALE_VARDIR@/lib/maxscale mkdir -p @MAXSCALE_VARDIR@/cache/maxscale @@ -37,3 +38,17 @@ then cp @CMAKE_INSTALL_PREFIX@/@MAXSCALE_SHAREDIR@/maxscale.service /usr/lib/systemd/system fi /sbin/ldconfig +mandb +cat <& 2 +********** Notice: MaxScale 1.2 Changes ************** + +MaxScale 1.2 has changed the default installation locations +and various files have changed locations. The configuration +file is now read from /etc/maxscale.cnf (note the lower case name) +and MaxScale data is in /var/lib/maxscale/. + +The default location of binary log files and the authentication cache changed from +/usr/local/mariadb-maxscale/ to /var/lib/maxscale/. + +****************************************************** +EOF diff --git a/server/core/config.c b/server/core/config.c index 274522468..3e778d077 100644 --- a/server/core/config.c +++ b/server/core/config.c @@ -345,6 +345,8 @@ hashtable_memory_fns(monitorhash,strdup,NULL,free,NULL); char *weightby; char *version_string; char *subservices; + char *ssl,*ssl_cert,*ssl_key,*ssl_ca_cert,*ssl_version; + char* ssl_cert_verify_depth; bool is_rwsplit = false; bool is_schemarouter = false; char *allow_localhost_match_wildcard_host; @@ -353,6 +355,12 @@ hashtable_memory_fns(monitorhash,strdup,NULL,free,NULL); user = config_get_value(obj->parameters, "user"); auth = config_get_value(obj->parameters, "passwd"); subservices = config_get_value(obj->parameters, "subservices"); + ssl = config_get_value(obj->parameters, "ssl"); + ssl_cert = config_get_value(obj->parameters, "ssl_cert"); + ssl_key = config_get_value(obj->parameters, "ssl_key"); + ssl_ca_cert = config_get_value(obj->parameters, "ssl_ca_cert"); + ssl_version = config_get_value(obj->parameters, "ssl_version"); + ssl_cert_verify_depth = config_get_value(obj->parameters, "ssl_cert_verify_depth"); enable_root_user = config_get_value( obj->parameters, "enable_root_user"); @@ -420,7 +428,7 @@ hashtable_memory_fns(monitorhash,strdup,NULL,free,NULL); /** Add the 5.5.5- string to the start of the version string if * the version string starts with "10.". - * This mimics MariaDB 10.0 replication which adds 5.5.5- for backwards compatibility. */ + * This mimics MariaDB 10.0 behavior which adds 5.5.5- for backwards compatibility. */ if(strncmp(version_string,"10.",3) == 0) { ((SERVICE *)(obj->element))->version_string = malloc((strlen(version_string) + @@ -443,7 +451,84 @@ hashtable_memory_fns(monitorhash,strdup,NULL,free,NULL); max_slave_rlag_str = config_get_value(obj->parameters, "max_slave_replication_lag"); - + + if(ssl) + { + if(ssl_cert == NULL) + { + error_count++; + skygw_log_write(LE,"Error: Server certificate missing for service '%s'." + "Please provide the path to the server certificate by adding the ssl_cert= parameter", + obj->object); + } + if(ssl_ca_cert == NULL) + { + error_count++; + skygw_log_write(LE,"Error: CA Certificate missing for service '%s'." + "Please provide the path to the certificate authority certificate by adding the ssl_ca_cert= parameter", + obj->object); + } + if(ssl_key == NULL) + { + error_count++; + skygw_log_write(LE,"Error: Server private key missing for service '%s'. " + "Please provide the path to the server certificate key by adding the ssl_key= parameter" + ,obj->object); + } + + if(access(ssl_ca_cert,F_OK) != 0) + { + skygw_log_write(LE,"Error: Certificate authority file for service '%s' not found: %s", + obj->object, + ssl_ca_cert); + error_count++; + } + if(access(ssl_cert,F_OK) != 0) + { + skygw_log_write(LE,"Error: Server certificate file for service '%s' not found: %s", + obj->object, + ssl_cert); + error_count++; + } + if(access(ssl_key,F_OK) != 0) + { + skygw_log_write(LE,"Error: Server private key file for service '%s' not found: %s", + obj->object, + ssl_key); + error_count++; + } + + if(error_count == 0) + { + if(serviceSetSSL(obj->element,ssl) != 0) + { + skygw_log_write(LE,"Error: Unknown parameter for service '%s': %s",obj->object,ssl); + error_count++; + } + else + { + serviceSetCertificates(obj->element,ssl_cert,ssl_key,ssl_ca_cert); + if(ssl_version) + { + if(serviceSetSSLVersion(obj->element,ssl_version) != 0) + { + skygw_log_write(LE,"Error: Unknown parameter value for 'ssl_version' for service '%s': %s",obj->object,ssl_version); + error_count++; + } + } + if(ssl_cert_verify_depth) + { + if(serviceSetSSLVerifyDepth(obj->element,atoi(ssl_cert_verify_depth)) != 0) + { + skygw_log_write(LE,"Error: Invalid parameter value for 'ssl_cert_verify_depth' for service '%s': %s",obj->object,ssl_cert_verify_depth); + error_count++; + } + } + } + } + + } + if (enable_root_user) serviceEnableRootUser( obj->element, @@ -1339,7 +1424,7 @@ int i; } else if (strcmp(name, "ms_timestamp") == 0) { - skygw_set_highp(config_truth_value(value)); + skygw_set_highp(config_truth_value((char*)value)); } else { @@ -1347,7 +1432,7 @@ int i; { if (strcasecmp(name, lognames[i].logname) == 0) { - if (config_truth_value(value)) + if (config_truth_value((char*)value)) skygw_log_enable(lognames[i].logfile); else skygw_log_disable(lognames[i].logfile); @@ -1925,6 +2010,12 @@ static char *service_params[] = "version_string", "filters", "weightby", + "ssl_cert", + "ssl_ca_cert", + "ssl", + "ssl_key", + "ssl_version", + "ssl_cert_verify_depth", NULL }; diff --git a/server/core/dcb.c b/server/core/dcb.c index 6717aea41..7f3651953 100644 --- a/server/core/dcb.c +++ b/server/core/dcb.c @@ -49,6 +49,7 @@ * backend * 07/05/2014 Mark Riddoch Addition of callback mechanism * 20/06/2014 Mark Riddoch Addition of dcb_clone + * 29/05/2015 Markus Makela Addition of dcb_write_SSL * * @endverbatim */ @@ -193,7 +194,8 @@ DCB *rval; rval->polloutbusy = 0; rval->writecheck = 0; rval->fd = DCBFD_CLOSED; - + rval->server = NULL; + rval->service = NULL; rval->evq.next = NULL; rval->evq.prev = NULL; rval->evq.pending_events = 0; @@ -432,7 +434,8 @@ DCB_CALLBACK *cb; free(cb); } spinlock_release(&dcb->cb_lock); - + if(dcb->ssl) + SSL_free(dcb->ssl); bitmask_free(&dcb->memdata.bitmask); free(dcb); } @@ -880,6 +883,364 @@ return_n: return n; } + +/** + * General purpose read routine to read data from a socket in the + * Descriptor Control Block and append it to a linked list of buffers. + * This function will read at most nbytes of data. + * + * The list may be empty, in which case *head == NULL. This + * + * @param dcb The DCB to read from + * @param head Pointer to linked list to append data to + * @param nbytes Maximum number of bytes read + * @return -1 on error, otherwise the number of read bytes on the last + * iteration of while loop. 0 is returned if no data available. + */ +int dcb_read_n( + DCB *dcb, + GWBUF **head, + int nbytes) +{ + GWBUF *buffer = NULL; + int b; + int rc; + int n; + int nread = 0; + + CHK_DCB(dcb); + + if (dcb->fd <= 0) + { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Read failed, dcb is %s.", + dcb->fd == DCBFD_CLOSED ? "closed" : "cloned, not readable"))); + n = 0; + goto return_n; + } + + int bufsize; + + rc = ioctl(dcb->fd, FIONREAD, &b); + + if (rc == -1) + { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : ioctl FIONREAD for dcb %p in " + "state %s fd %d failed due error %d, %s.", + dcb, + STRDCBSTATE(dcb->state), + dcb->fd, + errno, + strerror(errno)))); + n = -1; + goto return_n; + } + + if (b == 0 && nread == 0) + { + /** Handle closed client socket */ + if (dcb_isclient(dcb)) + { + char c; + int l_errno = 0; + int r = -1; + + /* try to read 1 byte, without consuming the socket buffer */ + r = recv(dcb->fd, &c, sizeof(char), MSG_PEEK); + l_errno = errno; + + if (r <= 0 && + l_errno != EAGAIN && + l_errno != EWOULDBLOCK && + l_errno != 0) + { + n = -1; + goto return_n; + } + } + n = 0; + goto return_n; + } + else if (b == 0) + { + n = 0; + goto return_n; + } + + dcb->last_read = hkheartbeat; + + bufsize = MIN(b, nbytes); + + if ((buffer = gwbuf_alloc(bufsize)) == NULL) + { + /*< + * This is a fatal error which should cause shutdown. + * Todo shutdown if memory allocation fails. + */ + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Failed to allocate read buffer " + "for dcb %p fd %d, due %d, %s.", + dcb, + dcb->fd, + errno, + strerror(errno)))); + + n = -1; + goto return_n; + } + GW_NOINTR_CALL(n = read(dcb->fd, GWBUF_DATA(buffer), bufsize); + dcb->stats.n_reads++); + + if (n <= 0) + { + if (errno != 0 && errno != EAGAIN && errno != EWOULDBLOCK) + { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Read failed, dcb %p in state " + "%s fd %d, due %d, %s.", + dcb, + STRDCBSTATE(dcb->state), + dcb->fd, + errno, + strerror(errno)))); + } + gwbuf_free(buffer); + goto return_n; + } + nread += n; + + LOGIF(LD, (skygw_log_write( + LOGFILE_DEBUG, + "%lu [dcb_read] Read %d bytes from dcb %p in state %s " + "fd %d.", + pthread_self(), + n, + dcb, + STRDCBSTATE(dcb->state), + dcb->fd))); + /*< Append read data to the gwbuf */ + *head = gwbuf_append(*head, buffer); + +return_n: + return n; +} + + +/** + * General purpose read routine to read data from a socket through the SSL + * structure lined with this DCB and append it to a linked list of buffers. + * The list may be empty, in which case *head == NULL. The SSL structure should + * be initialized and the SSL handshake should be done. + * + * @param dcb The DCB to read from + * @param head Pointer to linked list to append data to + * @return -1 on error, otherwise the number of read bytes on the last + * iteration of while loop. 0 is returned if no data available. + */ +int dcb_read_SSL( + DCB *dcb, + GWBUF **head) +{ + GWBUF *buffer = NULL; + int b,pending; + int rc; + int n; + int nread = 0; + int ssl_errno = 0; + CHK_DCB(dcb); + + if (dcb->fd <= 0) + { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Read failed, dcb is %s.", + dcb->fd == DCBFD_CLOSED ? "closed" : "cloned, not readable"))); + n = 0; + goto return_n; + } + + while (true) + { + int bufsize; + ssl_errno = 0; + rc = ioctl(dcb->fd, FIONREAD, &b); + pending = SSL_pending(dcb->ssl); + if (rc == -1) + { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : ioctl FIONREAD for dcb %p in " + "state %s fd %d failed due error %d, %s.", + dcb, + STRDCBSTATE(dcb->state), + dcb->fd, + errno, + strerror(errno)))); + n = -1; + goto return_n; + } + + if (b == 0 && pending == 0 && nread == 0) + { + /** Handle closed client socket */ + if (dcb_isclient(dcb)) + { + char c = 0; + int r = -1; + + /* try to read 1 byte, without consuming the socket buffer */ + r = SSL_peek(dcb->ssl, &c, sizeof(char)); + if (r <= 0) + { + ssl_errno = SSL_get_error(dcb->ssl,r); + if(ssl_errno != SSL_ERROR_WANT_READ && + ssl_errno != SSL_ERROR_WANT_WRITE && + ssl_errno != SSL_ERROR_NONE) + n = -1; + else + n = 0; + goto return_n; + } + } + n = 0; + goto return_n; + } + else if (b == 0 && pending == 0) + { + n = 0; + goto return_n; + } +#ifdef SS_DEBUG + else + { + skygw_log_write_flush(LD,"Total: %d Socket: %d Pending: %d", + nread,b,pending); + } +#endif + + dcb->last_read = hkheartbeat; + + bufsize = MIN(b, MAX_BUFFER_SIZE); + + if ((buffer = gwbuf_alloc(bufsize)) == NULL) + { + /*< + * This is a fatal error which should cause shutdown. + * Todo shutdown if memory allocation fails. + */ + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Failed to allocate read buffer " + "for dcb %p fd %d, due %d, %s.", + dcb, + dcb->fd, + errno, + strerror(errno)))); + + n = -1; + goto return_n; + } + + n = SSL_read(dcb->ssl, GWBUF_DATA(buffer), bufsize); + dcb->stats.n_reads++; + + if (n < 0) + { + char errbuf[200]; + ssl_errno = SSL_get_error(dcb->ssl,n); +#ifdef SS_DEBUG + if(ssl_errno == SSL_ERROR_SSL || + ssl_errno == SSL_ERROR_SYSCALL) + { + int eno; + while((eno = ERR_get_error()) != 0) + { + ERR_error_string(eno,errbuf); + skygw_log_write(LE, + "%s", + errbuf); + } + } +#endif + if(ssl_errno == SSL_ERROR_WANT_READ || + ssl_errno == SSL_ERROR_WANT_WRITE || + ssl_errno == SSL_ERROR_NONE) + { + n = 0; + } + else + { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Read failed, dcb %p in state " + "%s fd %d, SSL error %d: %s.", + dcb, + STRDCBSTATE(dcb->state), + dcb->fd, + ssl_errno, + strerror(errno)))); + + if(ssl_errno == SSL_ERROR_SSL || + ssl_errno == SSL_ERROR_SYSCALL) + { + while((ssl_errno = ERR_get_error()) != 0) + { + ERR_error_string(ssl_errno,errbuf); + skygw_log_write(LE, + "%s", + errbuf); + } + } + } + n = -1; + gwbuf_free(buffer); + goto return_n; + } + else if(n == 0) + { + gwbuf_free(buffer); + goto return_n; + } + + gwbuf_rtrim(buffer,bufsize - n); +#ifdef SS_DEBUG + skygw_log_write(LD,"%lu SSL: Truncated buffer from %d to %d bytes. " + "Read %d bytes, %d bytes waiting.\n",pthread_self(), + bufsize,GWBUF_LENGTH(buffer),n,b); + + if(GWBUF_LENGTH(buffer) != n){ + skygw_log_sync_all(); + } + + ss_info_dassert((buffer->start <= buffer->end),"Buffer start has passed end."); + ss_info_dassert(GWBUF_LENGTH(buffer) == n,"Buffer size not equal to read bytes."); +#endif + nread += n; + + LOGIF(LD, (skygw_log_write( + LOGFILE_DEBUG, + "%lu [dcb_read_SSL] Read %d bytes from dcb %p in state %s " + "fd %d.", + pthread_self(), + n, + dcb, + STRDCBSTATE(dcb->state), + dcb->fd))); + + /*< Append read data to the gwbuf */ + *head = gwbuf_append(*head, buffer); + rc = ioctl(dcb->fd, FIONREAD, &b); + pending = SSL_pending(dcb->ssl); + + } /*< while (true) */ +return_n: + return n; +} /** * General purpose routine to write to a DCB * @@ -905,11 +1266,11 @@ int below_water; return 0; } /** - * SESSION_STATE_STOPPING means that one of the backends is closing - * the router session. Some backends may have not completed + * SESSION_STATE_STOPPING means that one of the backends is closing + * the router session. Some backends may have not completed * authentication yet and thus they have no information about router * being closed. Session state is changed to SESSION_STATE_STOPPING - * before router's closeSession is called and that tells that DCB may + * before router's closeSession is called and that tells that DCB may * still be writable. */ if (queue == NULL || @@ -932,9 +1293,9 @@ int below_water; //ss_dassert(false); return 0; } - + spinlock_acquire(&dcb->writeqlock); - + if (dcb->writeq != NULL) { /* @@ -949,7 +1310,7 @@ int below_water; if (queue) { int qlen; - + qlen = gwbuf_length(queue); atomic_add(&dcb->writeqlen, qlen); dcb->writeq = gwbuf_append(dcb->writeq, queue); @@ -998,7 +1359,7 @@ int below_water; w = gw_write(dcb, GWBUF_DATA(queue), qlen); dcb->stats.n_writes++; ); - + if (w < 0) { saved_errno = errno; @@ -1006,7 +1367,7 @@ int below_water; if (LOG_IS_ENABLED(LOGFILE_DEBUG)) { - if (saved_errno == EPIPE) + if (saved_errno == EPIPE) { LOGIF(LD, (skygw_log_write( LOGFILE_DEBUG, @@ -1019,9 +1380,9 @@ int below_water; dcb->fd, saved_errno, strerror(saved_errno)))); - } + } } - + if (LOG_IS_ENABLED(LOGFILE_ERROR)) { if (saved_errno != EPIPE && @@ -1066,7 +1427,7 @@ int below_water; if (queue) { int qlen; - + qlen = gwbuf_length(queue); atomic_add(&dcb->writeqlen, qlen); dcb->stats.n_buffered++; @@ -1086,7 +1447,7 @@ int below_water; if (GWBUF_IS_TYPE_MYSQL(queue)) { uint8_t* data = GWBUF_DATA(queue); - + if (data[4] == 0x01) { dolog = false; @@ -1116,6 +1477,272 @@ int below_water; return 1; } +/** + * General purpose routine to write to an SSL enabled DCB + * + * @param dcb The DCB of the client + * @param ssl The SSL structure for this DCB + * @param queue Queue of buffers to write + * @return 0 on failure, 1 on success + */ +int +dcb_write_SSL(DCB *dcb, GWBUF *queue) +{ + int w; + int saved_errno = 0; + int below_water; + + below_water = (dcb->high_water && dcb->writeqlen < dcb->high_water) ? 1 : 0; + ss_dassert(queue != NULL); + + if (dcb->fd <= 0) + { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Write failed, dcb is %s.", + dcb->fd == DCBFD_CLOSED ? "closed" : "cloned, not writable"))); + return 0; + } + + /** + * SESSION_STATE_STOPPING means that one of the backends is closing + * the router session. Some backends may have not completed + * authentication yet and thus they have no information about router + * being closed. Session state is changed to SESSION_STATE_STOPPING + * before router's closeSession is called and that tells that DCB may + * still be writable. + */ + if (queue == NULL || + (dcb->state != DCB_STATE_ALLOC && + dcb->state != DCB_STATE_POLLING && + dcb->state != DCB_STATE_LISTENING && + dcb->state != DCB_STATE_NOPOLLING && + (dcb->session == NULL || + dcb->session->state != SESSION_STATE_STOPPING))) + { + LOGIF(LD, (skygw_log_write( + LOGFILE_DEBUG, + "%lu [dcb_write] Write aborted to dcb %p because " + "it is in state %s", + pthread_self(), + dcb->stats.n_buffered, + dcb, + STRDCBSTATE(dcb->state), + dcb->fd))); + //ss_dassert(false); + return 0; + } + + spinlock_acquire(&dcb->writeqlock); + + if (dcb->writeq != NULL) + { + /* + * We have some queued data, so add our data to + * the write queue and return. + * The assumption is that there will be an EPOLLOUT + * event to drain what is already queued. We are protected + * by the spinlock, which will also be acquired by the + * the routine that drains the queue data, so we should + * not have a race condition on the event. + */ + if (queue) + { + int qlen; + + qlen = gwbuf_length(queue); + atomic_add(&dcb->writeqlen, qlen); + dcb->writeq = gwbuf_append(dcb->writeq, queue); + dcb->stats.n_buffered++; + LOGIF(LD, (skygw_log_write( + LOGFILE_DEBUG, + "%lu [dcb_write] Append to writequeue. %d writes " + "buffered for dcb %p in state %s fd %d", + pthread_self(), + dcb->stats.n_buffered, + dcb, + STRDCBSTATE(dcb->state), + dcb->fd))); + } + } + else + { + /* + * Loop over the buffer chain that has been passed to us + * from the reading side. + * Send as much of the data in that chain as possible and + * add any balance to the write queue. + */ + while (queue != NULL) + { + int qlen; +#if defined(FAKE_CODE) + if (dcb->dcb_role == DCB_ROLE_REQUEST_HANDLER && + dcb->session != NULL) + { + if (dcb_isclient(dcb) && fail_next_client_fd) { + dcb_fake_write_errno[dcb->fd] = 32; + dcb_fake_write_ev[dcb->fd] = 29; + fail_next_client_fd = false; + } else if (!dcb_isclient(dcb) && + fail_next_backend_fd) + { + dcb_fake_write_errno[dcb->fd] = 32; + dcb_fake_write_ev[dcb->fd] = 29; + fail_next_backend_fd = false; + } + } +#endif /* FAKE_CODE */ + qlen = GWBUF_LENGTH(queue); + + w = gw_write_SSL(dcb->ssl, GWBUF_DATA(queue), qlen); + dcb->stats.n_writes++; + + if (w < 0) + { + int ssl_errno = SSL_get_error(dcb->ssl,w); + + if (LOG_IS_ENABLED(LOGFILE_DEBUG)) + { + switch(ssl_errno) + { + case SSL_ERROR_WANT_READ: + LOGIF(LD, (skygw_log_write( + LOGFILE_DEBUG, + "%lu [dcb_write] Write to dcb " + "%p in state %s fd %d failed " + "due error SSL_ERROR_WANT_READ", + pthread_self(), + dcb, + STRDCBSTATE(dcb->state), + dcb->fd))); + break; + case SSL_ERROR_WANT_WRITE: + LOGIF(LD, (skygw_log_write( + LOGFILE_DEBUG, + "%lu [dcb_write] Write to dcb " + "%p in state %s fd %d failed " + "due error SSL_ERROR_WANT_WRITE", + pthread_self(), + dcb, + STRDCBSTATE(dcb->state), + dcb->fd))); + break; + default: + LOGIF(LD, (skygw_log_write( + LOGFILE_DEBUG, + "%lu [dcb_write] Write to dcb " + "%p in state %s fd %d failed " + "due error %d", + pthread_self(), + dcb, + STRDCBSTATE(dcb->state), + dcb->fd,ssl_errno))); + if(ssl_errno == SSL_ERROR_SSL || + ssl_errno == SSL_ERROR_SYSCALL) + { + while((ssl_errno = ERR_get_error()) != 0) + { + char errbuf[140]; + ERR_error_string(ssl_errno,errbuf); + skygw_log_write(LE,"%s",errbuf); + } + } + break; + } + } + + if (LOG_IS_ENABLED(LOGFILE_ERROR)) + { + if (ssl_errno != 0) + { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Write to dcb %p in " + "state %s fd %d failed due " + "SSL error %d", + dcb, + STRDCBSTATE(dcb->state), + dcb->fd, + ssl_errno))); + } + } + break; + } + /* + * Pull the number of bytes we have written from + * queue with have. + */ + queue = gwbuf_consume(queue, w); + LOGIF(LD, (skygw_log_write( + LOGFILE_DEBUG, + "%lu [dcb_write] Wrote %d Bytes to dcb %p in " + "state %s fd %d", + pthread_self(), + w, + dcb, + STRDCBSTATE(dcb->state), + dcb->fd))); + } /*< while (queue != NULL) */ + /*< + * What wasn't successfully written is stored to write queue + * for suspended write. + */ + dcb->writeq = queue; + + if (queue) + { + int qlen; + + qlen = gwbuf_length(queue); + atomic_add(&dcb->writeqlen, qlen); + dcb->stats.n_buffered++; + } + } /* if (dcb->writeq) */ + + if (saved_errno != 0 && + queue != NULL && + saved_errno != EAGAIN && + saved_errno != EWOULDBLOCK) + { + bool dolog = true; + + /** + * Do not log if writing COM_QUIT to backend failed. + */ + if (GWBUF_IS_TYPE_MYSQL(queue)) + { + uint8_t* data = GWBUF_DATA(queue); + + if (data[4] == 0x01) + { + dolog = false; + } + } + if (dolog) + { + LOGIF(LD, (skygw_log_write( + LOGFILE_DEBUG, + "%lu [dcb_write] Writing to %s socket failed due %d, %s.", + pthread_self(), + dcb_isclient(dcb) ? "client" : "backend server", + saved_errno, + strerror(saved_errno)))); + } + spinlock_release(&dcb->writeqlock); + return 0; + } + spinlock_release(&dcb->writeqlock); + + if (dcb->high_water && dcb->writeqlen > dcb->high_water && below_water) + { + atomic_add(&dcb->stats.n_high_water, 1); + dcb_call_callback(dcb, DCB_REASON_HIGH_WATER); + } + + return 1; +} + /** * Drain the write queue of a DCB. This is called as part of the EPOLLOUT handling * of a socket and will try to send any buffered data from the write queue @@ -1208,6 +1835,86 @@ int above_water; return n; } + +/** + * Drain the write queue of a DCB. This is called as part of the EPOLLOUT handling + * of a socket and will try to send any buffered data from the write queue + * up until the point the write would block. This function uses SSL encryption + * and the SSL handshake should have been completed prior to calling this function. + * + * @param dcb DCB to drain the write queue of + * @return The number of bytes written + */ +int +dcb_drain_writeq_SSL(DCB *dcb) +{ + int n = 0; + int w; + int saved_errno = 0; + int above_water; + + above_water = (dcb->low_water && dcb->writeqlen > dcb->low_water) ? 1 : 0; + + spinlock_acquire(&dcb->writeqlock); + + if (dcb->writeq) + { + int len; + /* + * Loop over the buffer chain in the pending writeq + * Send as much of the data in that chain as possible and + * leave any balance on the write queue. + */ + while (dcb->writeq != NULL) + { + len = GWBUF_LENGTH(dcb->writeq); + w = gw_write_SSL(dcb->ssl, GWBUF_DATA(dcb->writeq), len); + + if (w < 0) + { + int ssl_errno = ERR_get_error(); + + if(ssl_errno == SSL_ERROR_WANT_WRITE || + ssl_errno == SSL_ERROR_WANT_ACCEPT || + ssl_errno == SSL_ERROR_WANT_READ) + { + break; + } + + skygw_log_write_flush( + LOGFILE_ERROR, + "Error : Write to dcb %p " + "in state %s fd %d failed: %s", + dcb, + STRDCBSTATE(dcb->state), + dcb->fd, + ERR_error_string(ssl_errno,NULL)); + break; + } + /* + * Pull the number of bytes we have written from + * queue with have. + */ + dcb->writeq = gwbuf_consume(dcb->writeq, w); + n += w; + } + } + spinlock_release(&dcb->writeqlock); + atomic_add(&dcb->writeqlen, -n); + + /* The write queue has drained, potentially need to call a callback function */ + if (dcb->writeq == NULL) + dcb_call_callback(dcb, DCB_REASON_DRAINED); + + if (above_water && dcb->writeqlen < dcb->low_water) + { + atomic_add(&dcb->stats.n_low_water, 1); + dcb_call_callback(dcb, DCB_REASON_LOW_WATER); + } + + return n; +} + /** * Removes dcb from poll set, and adds it to zombies list. As a consequense, * dcb first moves to DCB_STATE_NOPOLLING, and then to DCB_STATE_ZOMBIE state. @@ -1792,6 +2499,30 @@ static bool dcb_set_state_nomutex( return succp; } +/** + * Write data to a socket through an SSL structure. The SSL structure is linked to a DCB's socket + * and all communication is encrypted and done via the SSL structure. + * + * @param ssl The SSL structure to use for writing + * @param buf Buffer to write + * @param nbytes Number of bytes to write + * @return Number of written bytes + */ +int +gw_write_SSL(SSL* ssl, const void *buf, size_t nbytes) +{ + int w = 0; + int fd = SSL_get_fd(ssl); + + if (fd > 0) + { + w = SSL_write(ssl, buf, nbytes); + } + return w; +} + + + /** * Write data to a DCB * @@ -2225,3 +2956,201 @@ DCB *ptr; spinlock_release(&dcbspin); return rval; } + +/** + * Create the SSL structure for this DCB. + * This function creates the SSL structure for the given SSL context. This context + * should be the service's context + * @param dcb + * @param context + * @return + */ +int dcb_create_SSL(DCB* dcb) +{ + + if(serviceInitSSL(dcb->service) != 0) + { + return -1; + } + + if((dcb->ssl = SSL_new(dcb->service->ctx)) == NULL) + { + skygw_log_write(LE,"Error: Failed to initialize SSL for connection."); + return -1; + } + + if(SSL_set_fd(dcb->ssl,dcb->fd) == 0) + { + skygw_log_write(LE,"Error: Failed to set file descriptor for SSL connection."); + return -1; + } + + return 0; +} +/** + * Accept a SSL connection and do the SSL authentication handshake. + * This function accepts a client connection to a DCB. It assumes that the SSL + * structure has the underlying method of communication set and this method is ready + * for usage. It then proceeds with the SSL handshake and stops only if an error + * occurs or the client has not yet written enough data to complete the handshake. + * @param dcb DCB which should accept the SSL connection + * @return 1 if the handshake was successfully completed, 0 if the handshake is + * still ongoing and another call to dcb_SSL_accept should be made or -1 if an + * error occurred during the handshake and the connection should be terminated. + */ +int dcb_accept_SSL(DCB* dcb) +{ + int rval = 0,ssl_rval,errnum = 0,fd,b = 0,pending; + char errbuf[140]; + fd = dcb->fd; + + do + { + ssl_rval = SSL_accept(dcb->ssl); + errnum = SSL_get_error(dcb->ssl,ssl_rval); + LOGIF(LD,(skygw_log_write_flush(LD,"[dcb_accept_SSL] SSL_accept %d, error %d", + ssl_rval,errnum))); + switch(ssl_rval) + { + case 0: + errnum = SSL_get_error(dcb->ssl,ssl_rval); + skygw_log_write(LE,"Error: SSL authentication failed (SSL error %d):", + dcb, + dcb->remote, + errnum); + + if(errnum == SSL_ERROR_SSL || + errnum == SSL_ERROR_SYSCALL) + { + while((errnum = ERR_get_error()) != 0) + { + ERR_error_string(errnum,errbuf); + skygw_log_write(LE,"%s",errbuf); + } + } + rval = -1; + break; + case 1: + rval = 1; + LOGIF(LD,(skygw_log_write_flush(LD,"[dcb_accept_SSL] SSL_accept done for %s", + dcb->remote))); + return rval; + + case -1: + + errnum = SSL_get_error(dcb->ssl,ssl_rval); + + if(errnum == SSL_ERROR_WANT_READ || errnum == SSL_ERROR_WANT_WRITE) + { + /** Not all of the data has been read. Go back to the poll + queue and wait for more.*/ + rval = 0; + LOGIF(LD,(skygw_log_write_flush(LD,"[dcb_accept_SSL] SSL_accept ongoing for %s", + dcb->remote))); + return rval; + } + else + { + rval = -1; + skygw_log_write(LE, + "Error: Fatal error in SSL_accept for %s: (SSL version: %s SSL error code: %d)", + dcb->remote, + SSL_get_version(dcb->ssl), + errnum); + if(errnum == SSL_ERROR_SSL || + errnum == SSL_ERROR_SYSCALL) + { + while((errnum = ERR_get_error()) != 0) + { + ERR_error_string(errnum,errbuf); + skygw_log_write(LE, + "%s", + errbuf); + } + } + } + break; + + default: + skygw_log_write_flush(LE, + "Error: Fatal library error in SSL_accept, returned value was %d.", + ssl_rval); + rval = -1; + break; + } + ioctl(fd,FIONREAD,&b); + pending = SSL_pending(dcb->ssl); +#ifdef SS_DEBUG + skygw_log_write_flush(LD,"[dcb_accept_SSL] fd %d: %d bytes, %d pending",fd,b,pending); +#endif + }while((b > 0 || pending > 0) && rval != -1); + + return rval; +} + +/** + * Initiate an SSL client connection to a server + * + * This functions starts an SSL client connection to a server which is expecting + * an SSL handshake. The DCB should already have a TCP connection to the server and + * this connection should be in a state that expects an SSL handshake. + * @param dcb DCB to connect + * @return 1 on success, -1 on error and 0 if the SSL handshake is still ongoing + */ +int dcb_connect_SSL(DCB* dcb) +{ + int rval,errnum; + char errbuf[140]; + rval = SSL_connect(dcb->ssl); + + switch(rval) + { + case 0: + errnum = SSL_get_error(dcb->ssl,rval); + LOGIF(LD,(skygw_log_write_flush(LD,"SSL_connect shutdown for %s@%s", + dcb->user, + dcb->remote))); + return -1; + break; + case 1: + rval = 1; + LOGIF(LD,(skygw_log_write_flush(LD,"SSL_connect done for %s@%s", + dcb->user, + dcb->remote))); + return rval; + + case -1: + errnum = SSL_get_error(dcb->ssl,rval); + + if(errnum == SSL_ERROR_WANT_READ || errnum == SSL_ERROR_WANT_WRITE) + { + /** Not all of the data has been read. Go back to the poll + queue and wait for more.*/ + + rval = 0; + LOGIF(LD,(skygw_log_write_flush(LD,"SSL_connect ongoing for %s@%s", + dcb->user, + dcb->remote))); + } + else + { + rval = -1; + ERR_error_string(errnum,errbuf); + skygw_log_write_flush(LE, + "Error: Fatal error in SSL_accept for %s@%s: (SSL error code: %d) %s", + dcb->user, + dcb->remote, + errnum, + errbuf); + } + break; + + default: + skygw_log_write_flush(LE, + "Error: Fatal error in SSL_connect, returned value was %d.", + rval); + break; + } + + return rval; +} diff --git a/server/core/gateway.c b/server/core/gateway.c index 9845713e1..21c4472c0 100644 --- a/server/core/gateway.c +++ b/server/core/gateway.c @@ -40,7 +40,16 @@ * @endverbatim */ #define _XOPEN_SOURCE 700 +#define OPENSSL_THREAD_DEFINES #include + + #include + #if defined(OPENSSL_THREADS) +#define HAVE_OPENSSL_THREADS 1 + #else +#define HAVE_OPENSSL_THREADS 0 + #endif + #include #include #include @@ -197,6 +206,72 @@ static bool resolve_maxscale_conf_fname( static char* check_dir_access(char* dirname,bool,bool); static int set_user(); +/** SSL multi-threading functions and structures */ + +/** + * OpenSSL requires this struct to be defined in order to use dynamic locks + */ +struct CRYPTO_dynlock_value +{ + SPINLOCK lock; +}; + +/** + * Create a dynamic OpenSSL lock. The dynamic lock is just a wrapper structure + * around a SPINLOCK structure. + * @param file File name + * @param line Line number + * @return Pointer to new lock or NULL of an error occurred + */ +static struct CRYPTO_dynlock_value *ssl_create_dynlock(const char* file, int line) +{ + struct CRYPTO_dynlock_value* lock = malloc(sizeof(struct CRYPTO_dynlock_value)); + if(lock) + { + spinlock_init(&lock->lock); + } + return lock; +} + +/** + * Lock a dynamic lock for OpenSSL. + * @param mode + * @param n pointer to lock + * @param file File name + * @param line Line number + */ +static void ssl_lock_dynlock(int mode,struct CRYPTO_dynlock_value * n,const char* file, int line) +{ + if(mode & CRYPTO_LOCK) + { + spinlock_acquire(&n->lock); + } + else + { + spinlock_release(&n->lock); + } +} + +/** + * Free a dynamic OpenSSL lock. + * @param n Lock to free + * @param file File name + * @param line Line number + */ +static void ssl_free_dynlock(struct CRYPTO_dynlock_value * n,const char* file, int line) +{ + free(n); +} + +/** + * The thread ID callback function for OpenSSL dynamic locks. + * @param id Id to modify + */ +static void maxscale_ssl_id(CRYPTO_THREADID* id) +{ + CRYPTO_THREADID_set_numeric(id,pthread_self()); +} + /** * Handler for SIGHUP signal. Reload the configuration for the * gateway. @@ -935,7 +1010,7 @@ int main(int argc, char **argv) char* tmp_var; int option_index; int logtofile = 0; /* Use shared memory or file */ - int syslog_enabled = 1; /** Log to syslog */ + int syslog_enabled = 0; /** Log to syslog */ int maxscalelog_enabled = 1; /** Log with MaxScale */ ssize_t log_flush_timeout_ms = 0; sigset_t sigset; @@ -1078,26 +1153,34 @@ int main(int argc, char **argv) } break; case 'S': - if(strstr(optarg,"=")) - { - strtok(optarg,"= "); - maxscalelog_enabled = config_truth_value(strtok(NULL,"= ")); - } - else - { - maxscalelog_enabled = config_truth_value(optarg); - } + { + char* tok = strstr(optarg,"="); + if(tok) + { + tok++; + if(tok) + maxscalelog_enabled = config_truth_value(tok); + } + else + { + maxscalelog_enabled = config_truth_value(optarg); + } + } break; case 's': - if(strstr(optarg,"=")) - { - strtok(optarg,"= "); - syslog_enabled = config_truth_value(strtok(NULL,"= ")); - } - else - { - syslog_enabled = config_truth_value(optarg); - } + { + char* tok = strstr(optarg,"="); + if(tok) + { + tok++; + if(tok) + syslog_enabled = config_truth_value(tok); + } + else + { + syslog_enabled = config_truth_value(optarg); + } + } break; case 'U': if(set_user(optarg) != 0) @@ -1370,6 +1453,22 @@ int main(int argc, char **argv) rc = MAXSCALE_INTERNALERROR; goto return_main; } + + /** OpenSSL initialization */ + if(!HAVE_OPENSSL_THREADS) + { + char* logerr = "OpenSSL library does not support multi-threading"; + print_log_n_stderr(true, true, logerr, logerr, eno); + rc = MAXSCALE_INTERNALERROR; + goto return_main; + } + SSL_library_init(); + SSL_load_error_strings(); + OPENSSL_add_all_algorithms_noconf(); + CRYPTO_set_dynlock_create_callback(ssl_create_dynlock); + CRYPTO_set_dynlock_destroy_callback(ssl_free_dynlock); + CRYPTO_set_dynlock_lock_callback(ssl_lock_dynlock); + CRYPTO_THREADID_set_callback(maxscale_ssl_id); /* register exit function for embedded MySQL library */ l = atexit(libmysqld_done); @@ -2002,3 +2101,4 @@ static int set_user(char* user) return rval; } + diff --git a/server/core/service.c b/server/core/service.c index 5ba5d539d..4215ce725 100644 --- a/server/core/service.c +++ b/server/core/service.c @@ -69,6 +69,9 @@ extern int lm_enabled_logfiles_bitmask; extern size_t log_ses_count[]; extern __thread log_info_t tls_log_info; +static RSA *rsa_512 = NULL; +static RSA *rsa_1024 = NULL; + /** To be used with configuration type checks */ typedef struct typelib_st { int tl_nelems; @@ -136,7 +139,14 @@ SERVICE *service; service->routerModule = strdup(router); service->users_from_all = false; service->resources = NULL; - + service->ssl_mode = SSL_DISABLED; + service->ssl_init_done = false; + service->ssl_ca_cert = NULL; + service->ssl_cert = NULL; + service->ssl_key = NULL; + service->ssl_cert_verify_depth = DEFAULT_SSL_CERT_VERIFY_DEPTH; + /** Support the highest possible SSL/TLS methods available as the default */ + service->ssl_method_type = SERVICE_SSL_TLS_MAX; if (service->name == NULL || service->routerModule == NULL) { if (service->name) @@ -412,6 +422,17 @@ serviceStart(SERVICE *service) SERV_PROTOCOL *port; int listeners = 0; +if(service->ssl_mode != SSL_DISABLED) +{ + if(serviceInitSSL(service) != 0) + { + LOGIF(LE, (skygw_log_write_flush(LOGFILE_ERROR, + "%s: SSL initialization failed. Service not started.", + service->name))); + service->state = SERVICE_STATE_FAILED; + return 0; + } +} if ((service->router_instance = service->router->createInstance(service, service->routerOptions)) == NULL) { @@ -855,6 +876,99 @@ serviceOptimizeWildcard(SERVICE *service, int action) return 1; } +/** + * Set the locations of the server's SSL certificate, server's private key and the CA + * certificate which both the client and the server should trust. + * @param service Service to configure + * @param cert SSL certificate + * @param key SSL private key + * @param ca_cert SSL CA certificate + */ +void +serviceSetCertificates(SERVICE *service, char* cert,char* key, char* ca_cert) +{ + if(service->ssl_cert) + free(service->ssl_cert); + service->ssl_cert = strdup(cert); + + if(service->ssl_key) + free(service->ssl_key); + service->ssl_key = strdup(key); + + if(service->ssl_ca_cert) + free(service->ssl_ca_cert); + service->ssl_ca_cert = strdup(ca_cert); +} + +/** + * Set the maximum SSL/TLS version the service will support + * @param service Service to configure + * @param version SSL/TLS version string + * @return 0 on success, -1 on invalid version string + */ +int +serviceSetSSLVersion(SERVICE *service, char* version) +{ + if(strcasecmp(version,"SSLV2") == 0) + service->ssl_method_type = SERVICE_SSLV2; + else if(strcasecmp(version,"SSLV3") == 0) + service->ssl_method_type = SERVICE_SSLV3; + else if(strcasecmp(version,"TLSV10") == 0) + service->ssl_method_type = SERVICE_TLS10; + else if(strcasecmp(version,"TLSV11") == 0) + service->ssl_method_type = SERVICE_TLS11; + else if(strcasecmp(version,"TLSV12") == 0) + service->ssl_method_type = SERVICE_TLS12; + else if(strcasecmp(version,"MAX") == 0) + service->ssl_method_type = SERVICE_SSL_TLS_MAX; + else return -1; + return 0; +} + +/** + * Set the service's SSL certificate verification depth. Depth of 0 means the peer + * certificate, 1 is the CA and 2 is a higher CA and so on. + * @param service Service to configure + * @param depth Certificate verification depth + * @return 0 on success, -1 on incorrect depth value + */ +int serviceSetSSLVerifyDepth(SERVICE* service, int depth) +{ + if(depth < 0) + return -1; + + service->ssl_cert_verify_depth = depth; + return 0; +} + +/** + * Enable or disable the service SSL capability of a service. + * The SSL mode string passed as a parameter should be one of required, enabled + * or disabled. Required requires all connections to use SSL encryption, enabled + * allows both SSL and non-SSL connections and disabled does not use SSL encryption. + * If the service SSL mode is set to enabled, then the client will decide whether + * SSL encryption is used. + * @param service Service to configure + * @param action Mode string. One of required, enabled or disabled. + * @return 0 on success, -1 on error + */ +int +serviceSetSSL(SERVICE *service, char* action) +{ + int rval = 0; + + if(strcasecmp(action,"required") == 0) + service->ssl_mode = SSL_REQUIRED; + else if(strcasecmp(action,"enabled") == 0) + service->ssl_mode = SSL_ENABLED; + else if(strcasecmp(action,"disabled") == 0) + service->ssl_mode = SSL_DISABLED; + else + rval = -1; + + return rval; +} + /** * Whether to strip escape characters from the name of the database the client * is connecting to. @@ -1018,6 +1132,8 @@ int i; printf("\tUsers data: %p\n", (void *)service->users); printf("\tTotal connections: %d\n", service->stats.n_sessions); printf("\tCurrently connected: %d\n", service->stats.n_current); + printf("\tSSL: %s\n", service->ssl_mode == SSL_DISABLED ? "Disabled": + (service->ssl_mode == SSL_ENABLED ? "Enabled":"Required")); } /** @@ -1127,6 +1243,8 @@ int i; service->stats.n_sessions); dcb_printf(dcb, "\tCurrently connected: %d\n", service->stats.n_current); + dcb_printf(dcb,"\tSSL: %s\n", service->ssl_mode == SSL_DISABLED ? "Disabled": + (service->ssl_mode == SSL_ENABLED ? "Enabled":"Required")); } /** @@ -1775,3 +1893,142 @@ int *data; return set; } + + +/** + * The RSA ket generation callback function for OpenSSL. + * @param s SSL structure + * @param is_export Not used + * @param keylength Length of the key + * @return Pointer to RSA structure + */ + RSA *tmp_rsa_callback(SSL *s, int is_export, int keylength) + { + RSA *rsa_tmp=NULL; + + switch (keylength) { + case 512: + if (rsa_512) + rsa_tmp = rsa_512; + else { /* generate on the fly, should not happen in this example */ + rsa_tmp = RSA_generate_key(keylength,RSA_F4,NULL,NULL); + rsa_512 = rsa_tmp; /* Remember for later reuse */ + } + break; + case 1024: + if (rsa_1024) + rsa_tmp=rsa_1024; + break; + default: + /* Generating a key on the fly is very costly, so use what is there */ + if (rsa_1024) + rsa_tmp=rsa_1024; + else + rsa_tmp=rsa_512; /* Use at least a shorter key */ + } + return(rsa_tmp); + } + + /** + * Initialize the servce's SSL context. This sets up the generated RSA + * encryption keys, chooses the server encryption level and configures the server + * certificate, private key and certificate authority file. + * @param service + * @return + */ +int serviceInitSSL(SERVICE* service) +{ + DH* dh; + RSA* rsa; + + if(!service->ssl_init_done) + { + switch(service->ssl_method_type) + { + case SERVICE_SSLV2: + service->method = (SSL_METHOD*)SSLv2_server_method(); + break; + case SERVICE_SSLV3: + service->method = (SSL_METHOD*)SSLv3_server_method(); + break; + case SERVICE_TLS10: + service->method = (SSL_METHOD*)TLSv1_server_method(); + break; + case SERVICE_TLS11: + service->method = (SSL_METHOD*)TLSv1_1_server_method(); + break; + case SERVICE_TLS12: + service->method = (SSL_METHOD*)TLSv1_2_server_method(); + break; + + /** Rest of these use the maximum available SSL/TLS methods */ + case SERVICE_SSL_MAX: + service->method = (SSL_METHOD*)SSLv23_server_method(); + break; + case SERVICE_TLS_MAX: + service->method = (SSL_METHOD*)SSLv23_server_method(); + break; + case SERVICE_SSL_TLS_MAX: + service->method = (SSL_METHOD*)SSLv23_server_method(); + break; + default: + service->method = (SSL_METHOD*)SSLv23_server_method(); + break; + } + + service->ctx = SSL_CTX_new(service->method); + + /** Enable all OpenSSL bug fixes */ + SSL_CTX_set_options(service->ctx,SSL_OP_ALL); + + /** Generate the 512-bit and 1024-bit RSA keys */ + if(rsa_512 == NULL) + { + rsa_512 = RSA_generate_key(512,RSA_F4,NULL,NULL); + if (rsa_512 == NULL) + skygw_log_write(LE,"Error: 512-bit RSA key generation failed."); + } + if(rsa_1024 == NULL) + { + rsa_1024 = RSA_generate_key(1024,RSA_F4,NULL,NULL); + if (rsa_1024 == NULL) + skygw_log_write(LE,"Error: 1024-bit RSA key generation failed."); + } + + if(rsa_512 != NULL && rsa_1024 != NULL) + SSL_CTX_set_tmp_rsa_callback(service->ctx,tmp_rsa_callback); + + /** Load the server sertificate */ + if (SSL_CTX_use_certificate_file(service->ctx, service->ssl_cert, SSL_FILETYPE_PEM) <= 0) { + skygw_log_write(LE,"Error: Failed to set server SSL certificate."); + return -1; + } + + /* Load the private-key corresponding to the server certificate */ + if (SSL_CTX_use_PrivateKey_file(service->ctx, service->ssl_key, SSL_FILETYPE_PEM) <= 0) { + skygw_log_write(LE,"Error: Failed to set server SSL key."); + return -1; + } + + /* Check if the server certificate and private-key matches */ + if (!SSL_CTX_check_private_key(service->ctx)) { + skygw_log_write(LE,"Error: Server SSL certificate and key do not match."); + return -1; + } + + + /* Load the RSA CA certificate into the SSL_CTX structure */ + if (!SSL_CTX_load_verify_locations(service->ctx, service->ssl_ca_cert, NULL)) { + skygw_log_write(LE,"Error: Failed to set Certificate Authority file."); + return -1; + } + + /* Set to require peer (client) certificate verification */ + SSL_CTX_set_verify(service->ctx,SSL_VERIFY_PEER,NULL); + + /* Set the verification depth */ + SSL_CTX_set_verify_depth(service->ctx,service->ssl_cert_verify_depth); + service->ssl_init_done = true; + } + return 0; +} diff --git a/server/include/dcb.h b/server/include/dcb.h index 7eedfbec3..19f1e72ea 100644 --- a/server/include/dcb.h +++ b/server/include/dcb.h @@ -23,6 +23,9 @@ #include #include #include +#include +#include +#include #define ERRHANDLE @@ -265,6 +268,7 @@ typedef struct dcb { unsigned int high_water; /**< High water mark */ unsigned int low_water; /**< Low water mark */ struct server *server; /**< The associated backend server */ + SSL* ssl; /*< SSL struct for connection */ #if defined(SS_DEBUG) int dcb_port; /**< port of target server */ skygw_chk_t dcb_chk_tail; @@ -312,6 +316,7 @@ void dcb_free(DCB *); DCB *dcb_connect(struct server *, struct session *, const char *); DCB *dcb_clone(DCB *); int dcb_read(DCB *, GWBUF **); +int dcb_read_n(DCB*,GWBUF **,int); int dcb_drain_writeq(DCB *); void dcb_close(DCB *); DCB *dcb_process_zombies(int); /* Process Zombies except the one behind the pointer */ @@ -337,7 +342,13 @@ bool dcb_set_state(DCB* dcb, dcb_state_t new_state, dcb_state_t* old_state); void dcb_call_foreach (struct server* server, DCB_REASON reason); size_t dcb_get_session_id(DCB* dcb); bool dcb_get_ses_log_info(DCB* dcb, size_t* sesid, int* enabled_logs); - +int dcb_create_SSL(DCB* dcb); +int dcb_accept_SSL(DCB* dcb); +int dcb_connect_SSL(DCB* dcb); +int gw_write_SSL(SSL* ssl, const void *buf, size_t nbytes); +int dcb_write_SSL(DCB *dcb,GWBUF *queue); +int dcb_read_SSL(DCB *dcb,GWBUF **head); +int dcb_drain_writeq_SSL(DCB *dcb); /** diff --git a/server/include/gwdirs.h.in b/server/include/gwdirs.h.in index b8044484b..fe911c71b 100644 --- a/server/include/gwdirs.h.in +++ b/server/include/gwdirs.h.in @@ -18,17 +18,19 @@ * * Copyright MariaDB Corporation Ab 2015 */ - +#ifndef _GNU_SOURCE +#define _GNU_SOURCE 1 +#endif #include - +#include /** Default file locations, configured by CMake */ static const char* default_cnf_fname = "maxscale.cnf"; static const char* default_configdir = "/etc/"; -static const char* default_piddir = "/var/run/maxscale/"; -static const char* default_logdir = "/var/log/maxscale/"; -static const char* default_datadir = "/var/lib/maxscale/"; +static const char* default_piddir = "@MAXSCALE_VARDIR@/run/maxscale/"; +static const char* default_logdir = "@MAXSCALE_VARDIR@/log/maxscale/"; +static const char* default_datadir = "@MAXSCALE_VARDIR@/lib/maxscale/"; static const char* default_libdir = "@CMAKE_INSTALL_PREFIX@/@MAXSCALE_LIBDIR@"; -static const char* default_cachedir = "/var/cache/maxscale/"; +static const char* default_cachedir = "@MAXSCALE_VARDIR@/cache/maxscale/"; static const char* default_langdir = "@MAXSCALE_VARDIR@/lib/maxscale/"; static char* configdir = NULL; diff --git a/server/include/service.h b/server/include/service.h index f26c99806..085c0c595 100644 --- a/server/include/service.h +++ b/server/include/service.h @@ -26,7 +26,10 @@ #include #include #include - +#include +#include +#include +#include /** * @file service.h * @@ -105,6 +108,25 @@ typedef struct server_ref_t{ SERVER* server; }SERVER_REF; +typedef enum { + SSL_DISABLED, + SSL_ENABLED, + SSL_REQUIRED +} ssl_mode_t; + +enum{ + SERVICE_SSLV2, + SERVICE_SSLV3, + SERVICE_TLS10, + SERVICE_TLS11, + SERVICE_TLS12, + SERVICE_SSL_MAX, + SERVICE_TLS_MAX, + SERVICE_SSL_TLS_MAX +}; + +#define DEFAULT_SSL_CERT_VERIFY_DEPTH 100 /*< The default certificate verification depth */ + /** * Defines a service within the gateway. * @@ -149,8 +171,19 @@ typedef struct service { FILTER_DEF **filters; /**< Ordered list of filters */ int n_filters; /**< Number of filters */ int conn_timeout; /*< Session timeout in seconds */ + ssl_mode_t ssl_mode; /*< one of DISABLED, ENABLED or REQUIRED */ char *weightby; struct service *next; /**< The next service in the linked list */ + SSL_CTX *ctx; + SSL_METHOD *method; /*< SSLv2/3 or TLSv1/2 methods + * see: https://www.openssl.org/docs/ssl/SSL_CTX_new.html */ + int ssl_cert_verify_depth; /*< SSL certificate verification depth */ + int ssl_method_type; /*< Which of the SSLv2/3 or TLS1.0/1.1/1.2 methods to use */ + char* ssl_cert; /*< SSL certificate */ + char* ssl_key; /*< SSL private key */ + char* ssl_ca_cert; /*< SSL CA certificate */ + bool ssl_init_done; /*< If SSL has already been initialized for this service */ + } SERVICE; typedef enum count_spec_t {COUNT_NONE=0, COUNT_ATLEAST, COUNT_EXACT, COUNT_ATMOST} count_spec_t; @@ -178,6 +211,11 @@ extern int serviceRestart(SERVICE *); extern int serviceSetUser(SERVICE *, char *, char *); extern int serviceGetUser(SERVICE *, char **, char **); extern void serviceSetFilters(SERVICE *, char *); +extern int serviceSetSSL(SERVICE *service, char* action); +extern int serviceInitSSL(SERVICE* service); +extern int serviceSetSSLVersion(SERVICE *service, char* version); +extern int serviceSetSSLVerifyDepth(SERVICE* service, int depth); +extern void serviceSetCertificates(SERVICE *service, char* cert,char* key, char* ca_cert); extern int serviceEnableRootUser(SERVICE *, int ); extern int serviceSetTimeout(SERVICE *, int ); extern void serviceWeightBy(SERVICE *, char *); diff --git a/server/modules/filter/qlafilter.c b/server/modules/filter/qlafilter.c index a2d16d914..3c358cda1 100644 --- a/server/modules/filter/qlafilter.c +++ b/server/modules/filter/qlafilter.c @@ -305,7 +305,9 @@ char *remote, *userName; sprintf(my_session->filename, "%s.%d", my_instance->filebase, my_instance->sessions); - atomic_add(&my_instance->sessions,1); + + // Multiple sessions can try to update my_instance->sessions simultaneously + atomic_add(&(my_instance->sessions), 1); if (my_session->active) { diff --git a/server/modules/include/mysql_client_server_protocol.h b/server/modules/include/mysql_client_server_protocol.h index 46bbe296c..905f57dd3 100644 --- a/server/modules/include/mysql_client_server_protocol.h +++ b/server/modules/include/mysql_client_server_protocol.h @@ -54,7 +54,9 @@ #include #include #include - +#include +#include +#include #include #include #include @@ -89,6 +91,10 @@ #define COM_QUIT_PACKET_SIZE (4+1) struct dcb; +#define MYSQL_FAILED_AUTH 1 +#define MYSQL_FAILED_AUTH_DB 2 +#define MYSQL_FAILED_AUTH_SSL 3 + typedef enum { MYSQL_ALLOC, MYSQL_PENDING_CONNECT, @@ -97,6 +103,11 @@ typedef enum { MYSQL_AUTH_RECV, MYSQL_AUTH_FAILED, MYSQL_HANDSHAKE_FAILED, + MYSQL_AUTH_SSL_REQ, /*< client requested SSL but SSL_accept hasn't beed called */ + MYSQL_AUTH_SSL_HANDSHAKE_DONE, /*< SSL handshake has been fully completed */ + MYSQL_AUTH_SSL_HANDSHAKE_FAILED, /*< SSL handshake failed for any reason */ + MYSQL_AUTH_SSL_HANDSHAKE_ONGOING, /*< SSL_accept has been called but the + * SSL handshake hasn't been completed */ MYSQL_IDLE } mysql_auth_state_t; @@ -290,6 +301,7 @@ typedef struct { unsigned long tid; /*< MySQL Thread ID, in * handshake */ unsigned int charset; /*< MySQL character set at connect time */ + bool use_ssl; #if defined(SS_DEBUG) skygw_chk_t protocol_chk_tail; #endif @@ -309,7 +321,7 @@ typedef struct { #define MYSQL_IS_CHANGE_USER(payload) (MYSQL_GET_COMMAND(payload)==0x11) #define MYSQL_GET_NATTR(payload) ((int)payload[4]) -#endif /** _MYSQL_PROTOCOL_H */ + MySQLProtocol* mysql_protocol_init(DCB* dcb, int fd); void mysql_protocol_done (DCB* dcb); @@ -405,4 +417,4 @@ void init_response_status ( int* npackets, ssize_t* nbytes); - +#endif /** _MYSQL_PROTOCOL_H */ \ No newline at end of file diff --git a/server/modules/monitor/galeramon.c b/server/modules/monitor/galeramon.c index 6a842b8c1..07d9fd848 100644 --- a/server/modules/monitor/galeramon.c +++ b/server/modules/monitor/galeramon.c @@ -123,7 +123,7 @@ startMonitor(void *arg,void* opt) MONITOR* mon = arg; GALERA_MONITOR *handle = mon->handle; CONFIG_PARAMETER* params = (CONFIG_PARAMETER*)opt; - bool have_events = false; + bool have_events = false,script_error = false; if (handle != NULL) { handle->shutdown = 0; @@ -163,6 +163,7 @@ startMonitor(void *arg,void* opt) } else { + script_error = true; if(access(params->value,F_OK) == 0) { skygw_log_write(LE, @@ -175,17 +176,24 @@ startMonitor(void *arg,void* opt) "Error: The file cannot be found: %s", params->value); } - handle->script = NULL; } } else if(!strcmp(params->name,"events")) { - mon_parse_event_string((bool*)&handle->events,sizeof(handle->events),params->value); - have_events = true; + if(mon_parse_event_string((bool*)&handle->events,sizeof(handle->events),params->value) != 0) + script_error = true; + else + have_events = true; } params = params->next; } - + if(script_error) + { + skygw_log_write(LE,"Error: Errors were found in the script configuration parameters " + "for the monitor '%s'. The script will not be used.",mon->name); + free(handle->script); + handle->script = NULL; + } /** If no specific events are given, enable them all */ if(!have_events) { diff --git a/server/modules/monitor/mmmon.c b/server/modules/monitor/mmmon.c index 130cdb279..c0d54595d 100644 --- a/server/modules/monitor/mmmon.c +++ b/server/modules/monitor/mmmon.c @@ -113,7 +113,7 @@ startMonitor(void *arg,void* opt) MONITOR* mon = (MONITOR*)arg; MM_MONITOR *handle = mon->handle; CONFIG_PARAMETER* params = (CONFIG_PARAMETER*)opt; - bool have_events = false; + bool have_events = false,script_error = false; if (handle) { @@ -148,6 +148,7 @@ startMonitor(void *arg,void* opt) } else { + script_error = true; if(access(params->value,F_OK) == 0) { skygw_log_write(LE, @@ -165,11 +166,20 @@ startMonitor(void *arg,void* opt) } else if(!strcmp(params->name,"events")) { - mon_parse_event_string((bool*)&handle->events,sizeof(handle->events),params->value); - have_events = true; + if(mon_parse_event_string((bool*)&handle->events,sizeof(handle->events),params->value) != 0) + script_error = true; + else + have_events = true; } params = params->next; } + if(script_error) + { + skygw_log_write(LE,"Error: Errors were found in the script configuration parameters " + "for the monitor '%s'. The script will not be used.",mon->name); + free(handle->script); + handle->script = NULL; + } /** If no specific events are given, enable them all */ if(!have_events) { diff --git a/server/modules/monitor/monitor_common.c b/server/modules/monitor/monitor_common.c index e4b8e7dac..f4566c35a 100644 --- a/server/modules/monitor/monitor_common.c +++ b/server/modules/monitor/monitor_common.c @@ -343,7 +343,10 @@ int mon_parse_event_string(bool* events, size_t count,char* string) { event = mon_name_to_event(tok); if(event == UNDEFINED_MONITOR_EVENT) + { + skygw_log_write(LE,"Error: Invalid event name %s",tok); return -1; + } events[event] = true; tok = strtok_r(NULL,",| ",&saved); } diff --git a/server/modules/monitor/mysql_mon.c b/server/modules/monitor/mysql_mon.c index 6f10b0ac8..e310e2ede 100644 --- a/server/modules/monitor/mysql_mon.c +++ b/server/modules/monitor/mysql_mon.c @@ -140,7 +140,7 @@ startMonitor(void *arg, void* opt) MONITOR* monitor = (MONITOR*)arg; MYSQL_MONITOR *handle = (MYSQL_MONITOR*)monitor->handle; CONFIG_PARAMETER* params = (CONFIG_PARAMETER*)opt; - bool have_events = false; + bool have_events = false,script_error = false; if (handle) { @@ -176,6 +176,7 @@ startMonitor(void *arg, void* opt) } else { + script_error = true; if(access(params->value,F_OK) == 0) { skygw_log_write(LE, @@ -193,11 +194,20 @@ startMonitor(void *arg, void* opt) } else if(!strcmp(params->name,"events")) { - mon_parse_event_string(handle->events,sizeof(handle->events),params->value); - have_events = true; + if(mon_parse_event_string((bool*)&handle->events,sizeof(handle->events),params->value) != 0) + script_error = true; + else + have_events = true; } params = params->next; } + if(script_error) + { + skygw_log_write(LE,"Error: Errors were found in the script configuration parameters " + "for the monitor '%s'. The script will not be used.",monitor->name); + free(handle->script); + handle->script = NULL; + } /** If no specific events are given, enable them all */ if(!have_events) { @@ -313,6 +323,8 @@ char *server_string; int read_timeout = mon->read_timeout; int write_timeout = mon->write_timeout; + if(database->con) + mysql_close(database->con); database->con = mysql_init(NULL); mysql_options(database->con, MYSQL_OPT_CONNECT_TIMEOUT, (void *)&connect_timeout); diff --git a/server/modules/monitor/ndbclustermon.c b/server/modules/monitor/ndbclustermon.c index c8790be59..9561e275e 100644 --- a/server/modules/monitor/ndbclustermon.c +++ b/server/modules/monitor/ndbclustermon.c @@ -111,7 +111,7 @@ startMonitor(void *arg,void* opt) MONITOR* mon = (MONITOR*)arg; MYSQL_MONITOR *handle = mon->handle; CONFIG_PARAMETER* params = (CONFIG_PARAMETER*)opt; - bool have_events = false; + bool have_events = false,script_error = false; if (handle != NULL) { @@ -140,6 +140,7 @@ startMonitor(void *arg,void* opt) } else { + script_error = true; if(access(params->value,F_OK) == 0) { skygw_log_write(LE, @@ -157,10 +158,19 @@ startMonitor(void *arg,void* opt) } else if(!strcmp(params->name,"events")) { - mon_parse_event_string(&handle->events,sizeof(handle->events),params->value); - have_events = true; + if(mon_parse_event_string((bool*)&handle->events,sizeof(handle->events),params->value) != 0) + script_error = true; + else + have_events = true; } params = params->next; + } + if(script_error) + { + skygw_log_write(LE,"Error: Errors were found in the script configuration parameters " + "for the monitor '%s'. The script will not be used.",mon->name); + free(handle->script); + handle->script = NULL; } /** If no specific events are given, enable them all */ if(!have_events) diff --git a/server/modules/protocol/CMakeLists.txt b/server/modules/protocol/CMakeLists.txt index 4ae3b8f2c..124071c44 100644 --- a/server/modules/protocol/CMakeLists.txt +++ b/server/modules/protocol/CMakeLists.txt @@ -17,6 +17,7 @@ install(TARGETS HTTPD DESTINATION ${MAXSCALE_LIBDIR}) if(BUILD_TESTS) add_library(testprotocol SHARED testprotocol.c) install(TARGETS testprotocol DESTINATION ${MAXSCALE_LIBDIR}) + add_subdirectory(test) endif() add_library(maxscaled SHARED maxscaled.c) diff --git a/server/modules/protocol/mysql_backend.c b/server/modules/protocol/mysql_backend.c index ba5786851..d000474ab 100644 --- a/server/modules/protocol/mysql_backend.c +++ b/server/modules/protocol/mysql_backend.c @@ -72,7 +72,7 @@ static void backend_set_delayqueue(DCB *dcb, GWBUF *queue); static int gw_change_user(DCB *backend_dcb, SERVER *server, SESSION *in_session, GWBUF *queue); static GWBUF* process_response_data (DCB* dcb, GWBUF* readbuf, int nbytes_to_process); extern char* create_auth_failed_msg( GWBUF* readbuf, char* hostaddr, uint8_t* sha1); -extern char* create_auth_fail_str(char *username, char *hostaddr, char *sha1, char *db); +extern char* create_auth_fail_str(char *username, char *hostaddr, char *sha1, char *db,int); static bool sescmd_response_complete(DCB* dcb); @@ -1433,7 +1433,7 @@ static int gw_change_user( message = create_auth_fail_str(username, backend->session->client->remote, password_set, - ""); + "",auth_ret); if (message == NULL) { LOGIF(LE, (skygw_log_write_flush( diff --git a/server/modules/protocol/mysql_client.c b/server/modules/protocol/mysql_client.c index abdb4422c..c3e463139 100644 --- a/server/modules/protocol/mysql_client.c +++ b/server/modules/protocol/mysql_client.c @@ -37,7 +37,7 @@ * 09/09/2014 Massimiliano Pinto Added: 777 permission for socket path * 13/10/2014 Massimiliano Pinto Added: dbname authentication check * 10/11/2014 Massimiliano Pinto Added: client charset added to protocol struct - * + * 29/05/2015 Markus Makela Added SSL support */ #include #include @@ -46,6 +46,7 @@ #include #include #include +#include MODULE_INFO info = { MODULE_API_PROTOCOL, @@ -69,14 +70,18 @@ static int gw_MySQLWrite_client(DCB *dcb, GWBUF *queue); static int gw_error_client_event(DCB *dcb); static int gw_client_close(DCB *dcb); static int gw_client_hangup_event(DCB *dcb); - +int gw_read_client_event_SSL(DCB* dcb); +int gw_MySQLWrite_client_SSL(DCB *dcb, GWBUF *queue); +int gw_write_client_event_SSL(DCB *dcb); int mysql_send_ok(DCB *dcb, int packet_number, int in_affected_rows, const char* mysql_message); int MySQLSendHandshake(DCB* dcb); -static int gw_mysql_do_authentication(DCB *dcb, GWBUF *queue); +static int gw_mysql_do_authentication(DCB *dcb, GWBUF **queue); static int route_by_statement(SESSION *, GWBUF **); extern char* get_username_from_auth(char* ptr, uint8_t* data); extern int check_db_name_after_auth(DCB *, char *, int); -extern char* create_auth_fail_str(char *username, char *hostaddr, char *sha1, char *db); +extern char* create_auth_fail_str(char *username, char *hostaddr, char *sha1, char *db,int); + +int do_ssl_accept(MySQLProtocol* protocol); /* * The "module object" for the mysqld client protocol module. @@ -242,7 +247,7 @@ MySQLSendHandshake(DCB* dcb) char server_scramble[GW_MYSQL_SCRAMBLE_SIZE + 1]=""; char *version_string; int len_version_string=0; - + MySQLProtocol *protocol = DCB_PROTOCOL(dcb, MySQLProtocol); GWBUF *buf; @@ -319,7 +324,16 @@ MySQLSendHandshake(DCB* dcb) mysql_server_capabilities_one[0] &= ~GW_MYSQL_CAPABILITIES_COMPRESS; - mysql_server_capabilities_one[0] &= ~GW_MYSQL_CAPABILITIES_SSL; + + if(dcb->service->ssl_mode != SSL_DISABLED) + { + mysql_server_capabilities_one[1] |= GW_MYSQL_CAPABILITIES_SSL >> 8; + } + else + { + mysql_server_capabilities_one[0] &= ~GW_MYSQL_CAPABILITIES_SSL; + } + memcpy(mysql_handshake_payload, mysql_server_capabilities_one, sizeof(mysql_server_capabilities_one)); mysql_handshake_payload = mysql_handshake_payload + sizeof(mysql_server_capabilities_one); @@ -375,21 +389,24 @@ MySQLSendHandshake(DCB* dcb) /** * gw_mysql_do_authentication * - * Performs the MySQL protocol 4.1 authentication, using data in GWBUF *queue + * Performs the MySQL protocol 4.1 authentication, using data in GWBUF **queue. * * (MYSQL_session*)client_data including: user, db, client_sha1 are copied into - * the dcb->data and later to dcb->session->data. - * - * client_capabilitiesa are copied into the dcb->protocol + * the dcb->data and later to dcb->session->data. client_capabilities are copied + * into the dcb->protocol. + * + * If SSL is enabled for the service, the SSL handshake will be done before the + * MySQL authentication. * * @param dcb Descriptor Control Block of the client - * @param queue The GWBUF with data from client + * @param queue Pointer to the location of the GWBUF with data from client * @return 0 If succeed, otherwise non-zero value * * @note in case of failure, dcb->data is freed before returning. If succeed, * dcb->data is freed in session.c:session_free. */ -static int gw_mysql_do_authentication(DCB *dcb, GWBUF *queue) { +static int gw_mysql_do_authentication(DCB *dcb, GWBUF **buf) { + GWBUF* queue = *buf; MySQLProtocol *protocol = NULL; /* int compress = -1; */ int connect_with_db = -1; @@ -402,7 +419,7 @@ static int gw_mysql_do_authentication(DCB *dcb, GWBUF *queue) { uint8_t *stage1_hash = NULL; int auth_ret = -1; MYSQL_session *client_data = NULL; - + int ssl = 0; CHK_DCB(dcb); protocol = DCB_PROTOCOL(dcb, MySQLProtocol); @@ -437,7 +454,7 @@ static int gw_mysql_do_authentication(DCB *dcb, GWBUF *queue) { /* Detect now if there are enough bytes to continue */ if (client_auth_packet_size < (4 + 4 + 4 + 1 + 23)) { - return 1; + return MYSQL_FAILED_AUTH; } memcpy(&protocol->client_capabilities, client_auth_packet + 4, 4); @@ -451,11 +468,66 @@ static int gw_mysql_do_authentication(DCB *dcb, GWBUF *queue) { &protocol->client_capabilities); */ + /** Skip this if the SSL handshake is already done. + * If not, start the SSL handshake. */ + if(protocol->protocol_auth_state != MYSQL_AUTH_SSL_HANDSHAKE_DONE) + { + + ssl = protocol->client_capabilities & GW_MYSQL_CAPABILITIES_SSL; + + /** Client didn't requested SSL when SSL mode was required*/ + if(!ssl && protocol->owner_dcb->service->ssl_mode == SSL_REQUIRED) + { + LOGIF(LT,(skygw_log_write(LT,"User %s@%s connected to service '%s' without SSL when SSL was required.", + protocol->owner_dcb->user, + protocol->owner_dcb->remote, + protocol->owner_dcb->service->name))); + return MYSQL_FAILED_AUTH_SSL; + } + + if(LOG_IS_ENABLED(LT) && ssl) + { + skygw_log_write(LT,"User %s@%s connected to service '%s' with SSL.", + protocol->owner_dcb->user, + protocol->owner_dcb->remote, + protocol->owner_dcb->service->name); + } + + /** Do the SSL Handshake */ + if(ssl && protocol->owner_dcb->service->ssl_mode != SSL_DISABLED) + { + protocol->protocol_auth_state = MYSQL_AUTH_SSL_REQ; + + if(do_ssl_accept(protocol) < 0) + { + return MYSQL_FAILED_AUTH; + } + else + { + return 0; + } + } + else if(dcb->service->ssl_mode == SSL_ENABLED) + { + /** This is a non-SSL connection to a SSL enabled service. + * We have only read enough of the packet to know that the client + * is not requesting SSL and the rest of the auth packet is still + * waiting in the socket. We need to read the data from the socket + * to find out the username of the connecting client. */ + int bytes = dcb_read(dcb,&queue); + queue = gwbuf_make_contiguous(queue); + client_auth_packet = GWBUF_DATA(queue); + client_auth_packet_size = gwbuf_length(queue); + *buf = queue; + LOGIF(LD,(skygw_log_write(LD,"%lu Read %d bytes from fd %d",pthread_self(),bytes,dcb->fd))); + } + } + username = get_username_from_auth(username, client_auth_packet); if (username == NULL) { - return 1; + return MYSQL_FAILED_AUTH; } /* get charset */ @@ -557,6 +629,24 @@ gw_MySQLWrite_client(DCB *dcb, GWBUF *queue) return dcb_write(dcb, queue); } + +/** + * Write function for client DCB: writes data from MaxScale to Client using SSL + * encryption. The SSH handshake must have already been done. + * + * @param dcb The DCB of the client + * @param queue Queue of buffers to write + */ +int +gw_MySQLWrite_client_SSL(DCB *dcb, GWBUF *queue) +{ + MySQLProtocol *protocol = NULL; + CHK_DCB(dcb); + protocol = DCB_PROTOCOL(dcb, MySQLProtocol); + CHK_PROTOCOL(protocol); + return dcb_write_SSL(dcb, queue); +} + /** * Client read event triggered by EPOLLIN * @@ -580,9 +670,66 @@ int gw_read_client_event( CHK_DCB(dcb); protocol = DCB_PROTOCOL(dcb, MySQLProtocol); CHK_PROTOCOL(protocol); - rc = dcb_read(dcb, &read_buffer); - - + +#ifdef SS_DEBUG + skygw_log_write(LD,"[gw_read_client_event] Protocol state: %s", + gw_mysql_protocol_state2string(protocol->protocol_auth_state)); + +#endif + + /** SSL authentication is still going on, we need to call do_ssl_accept + * until it return 1 for success or -1 for error */ + if(protocol->protocol_auth_state == MYSQL_AUTH_SSL_HANDSHAKE_ONGOING || + protocol->protocol_auth_state == MYSQL_AUTH_SSL_REQ) + { + + switch(do_ssl_accept(protocol)) + { + case 0: + return 0; + break; + case 1: + { + int b = 0; + ioctl(dcb->fd,FIONREAD,&b); + if(b == 0) + { + skygw_log_write(LD, + "[gw_read_client_event] No data in socket after SSL auth"); + return 0; + } + break; + } + + case -1: + return 1; + break; + default: + return 1; + break; + } + } + + if(protocol->use_ssl) + { + /** SSL handshake is done, communication is now encrypted with SSL */ + rc = dcb_read_SSL(dcb, &read_buffer); + } + else if(dcb->service->ssl_mode != SSL_DISABLED && + protocol->protocol_auth_state == MYSQL_AUTH_SENT) + { + /** The service allows both SSL and non-SSL connections. + * read only enough of the auth packet to know if the client is + * requesting SSL. If the client is not requesting SSL the rest of + the auth packet will be read later. */ + rc = dcb_read_n(dcb, &read_buffer,(4 + 4 + 4 + 1 + 23)); + } + else + { + /** Normal non-SSL connection */ + rc = dcb_read(dcb, &read_buffer); + } + if (rc < 0) { dcb_close(dcb); @@ -690,8 +837,8 @@ int gw_read_client_event( dcb->dcb_readqueue = gwbuf_append(dcb->dcb_readqueue, read_buffer); nbytes_read = gwbuf_length(dcb->dcb_readqueue); data = (uint8_t *)GWBUF_DATA(dcb->dcb_readqueue); - - if (nbytes_read < 3 || nbytes_read < MYSQL_GET_PACKET_LEN(data)) + int plen = MYSQL_GET_PACKET_LEN(data); + if (nbytes_read < 3 || nbytes_read < MYSQL_GET_PACKET_LEN(data) + 4) { rc = 0; goto return_rc; @@ -719,7 +866,7 @@ int gw_read_client_event( } } - + /** * Now there should be at least one complete mysql packet in read_buffer. */ @@ -729,8 +876,19 @@ int gw_read_client_event( { int auth_val; - auth_val = gw_mysql_do_authentication(dcb, read_buffer); - + auth_val = gw_mysql_do_authentication(dcb, &read_buffer); + + if(protocol->protocol_auth_state == MYSQL_AUTH_SSL_REQ || + protocol->protocol_auth_state == MYSQL_AUTH_SSL_HANDSHAKE_ONGOING || + protocol->protocol_auth_state == MYSQL_AUTH_SSL_HANDSHAKE_DONE || + protocol->protocol_auth_state == MYSQL_AUTH_SSL_HANDSHAKE_FAILED) + { + /** SSL was requested and the handshake is either done or + * still ongoing. After the handshake is done, the client + * will send another auth packet. */ + break; + } + if (auth_val == 0) { SESSION *session; @@ -796,7 +954,7 @@ int gw_read_client_event( fail_str = create_auth_fail_str((char *)((MYSQL_session *)dcb->data)->user, dcb->remote, (char*)((MYSQL_session *)dcb->data)->client_sha1, - (char*)((MYSQL_session *)dcb->data)->db); + (char*)((MYSQL_session *)dcb->data)->db,auth_val); modutil_send_mysql_err_packet(dcb, 2, 0, 1045, "28000", fail_str); } if (fail_str) @@ -824,6 +982,113 @@ int gw_read_client_event( } break; + case MYSQL_AUTH_SSL_HANDSHAKE_DONE: + { + int auth_val; + + auth_val = gw_mysql_do_authentication(dcb, &read_buffer); + + + if (auth_val == 0) + { + SESSION *session; + + protocol->protocol_auth_state = MYSQL_AUTH_RECV; + /** + * Create session, and a router session for it. + * If successful, there will be backend connection(s) + * after this point. + */ + session = session_alloc(dcb->service, dcb); + + if (session != NULL) + { + CHK_SESSION(session); + ss_dassert(session->state != SESSION_STATE_ALLOC); + + protocol->protocol_auth_state = MYSQL_IDLE; + /** + * Send an AUTH_OK packet to the client, + * packet sequence is # 2 + */ + mysql_send_ok(dcb, 3, 0, NULL); + } + else + { + protocol->protocol_auth_state = MYSQL_AUTH_FAILED; + LOGIF(LD, (skygw_log_write( + LOGFILE_DEBUG, + "%lu [gw_read_client_event] session " + "creation failed. fd %d, " + "state = MYSQL_AUTH_FAILED.", + pthread_self(), + protocol->owner_dcb->fd))); + + /** Send ERR 1045 to client */ + mysql_send_auth_error( + dcb, + 3, + 0, + "failed to create new session"); + + dcb_close(dcb); + } + } + else + { + char* fail_str = NULL; + + protocol->protocol_auth_state = MYSQL_AUTH_FAILED; + + if (auth_val == 2) { + /** Send error 1049 to client */ + int message_len = 25 + MYSQL_DATABASE_MAXLEN; + + fail_str = calloc(1, message_len+1); + snprintf(fail_str, message_len, "Unknown database '%s'", + (char*)((MYSQL_session *)dcb->data)->db); + + modutil_send_mysql_err_packet(dcb, 3, 0, 1049, "42000", fail_str); + }else if(auth_val == 3){ + /** Send error 1045 to client */ + fail_str = create_auth_fail_str((char *)((MYSQL_session *)dcb->data)->user, + dcb->remote, + (char*)((MYSQL_session *)dcb->data)->client_sha1, + (char*)((MYSQL_session *)dcb->data)->db,auth_val); + modutil_send_mysql_err_packet(dcb, 3, 0, 1045, "28000", fail_str); + }else { + /** Send error 1045 to client */ + fail_str = create_auth_fail_str((char *)((MYSQL_session *)dcb->data)->user, + dcb->remote, + (char*)((MYSQL_session *)dcb->data)->client_sha1, + (char*)((MYSQL_session *)dcb->data)->db,auth_val); + modutil_send_mysql_err_packet(dcb, 3, 0, 1045, "28000", fail_str); + } + if (fail_str) + free(fail_str); + + LOGIF(LD, (skygw_log_write( + LOGFILE_DEBUG, + "%lu [gw_read_client_event] after " + "gw_mysql_do_authentication, fd %d, " + "state = MYSQL_AUTH_FAILED.", + protocol->owner_dcb->fd, + pthread_self()))); + /** + * Release MYSQL_session since it is not used anymore. + */ + if (!DCB_IS_CLONE(dcb)) + { + free(dcb->data); + } + dcb->data = NULL; + + dcb_close(dcb); + } + read_buffer = gwbuf_consume(read_buffer, nbytes_read); + } + break; + case MYSQL_IDLE: { uint8_t* payload = NULL; @@ -943,12 +1208,13 @@ return_rc: return rc; } + /////////////////////////////////////////////// // client write event to Client triggered by EPOLLOUT ////////////////////////////////////////////// -/** +/** * @node Client's fd became writable, and EPOLLOUT event - * arrived. As a consequence, client input buffer (writeq) is flushed. + * arrived. As a consequence, client input buffer (writeq) is flushed. * * Parameters: * @param dcb - in, use @@ -956,7 +1222,7 @@ return_rc: * * @return constantly 1 * - * + * * @details (write detailed description here) * */ @@ -966,6 +1232,53 @@ int gw_write_client_event(DCB *dcb) CHK_DCB(dcb); + ss_dassert(dcb->state != DCB_STATE_DISCONNECTED); + + if (dcb == NULL) { + goto return_1; + } + + if (dcb->state == DCB_STATE_DISCONNECTED) { + goto return_1; + } + + if (dcb->protocol == NULL) { + goto return_1; + } + protocol = (MySQLProtocol *)dcb->protocol; + CHK_PROTOCOL(protocol); + + if (protocol->protocol_auth_state == MYSQL_IDLE) + { + dcb_drain_writeq(dcb); + goto return_1; + } + +return_1: +#if defined(SS_DEBUG) + if (dcb->state == DCB_STATE_POLLING || + dcb->state == DCB_STATE_NOPOLLING || + dcb->state == DCB_STATE_ZOMBIE) + { + CHK_PROTOCOL(protocol); + } +#endif + return 1; +} + +/** + * EPOLLOUT event arrived and as a consequence, client input buffer (writeq) is + * flushed. The data is encrypted and SSL is used. The SSL handshake must have + * been successfully completed prior to this function being called. + * @param client dcb + * @return constantly 1 + */ +int gw_write_client_event_SSL(DCB *dcb) +{ + MySQLProtocol *protocol = NULL; + + CHK_DCB(dcb); + ss_dassert(dcb->state != DCB_STATE_DISCONNECTED); if (dcb == NULL) { @@ -984,7 +1297,7 @@ int gw_write_client_event(DCB *dcb) if (protocol->protocol_auth_state == MYSQL_IDLE) { - dcb_drain_writeq(dcb); + dcb_drain_writeq_SSL(dcb); goto return_1; } @@ -1064,6 +1377,9 @@ int gw_MySQLListener( LOGIF(LE, (skygw_log_write_flush(LOGFILE_ERROR,"Error: Failed to set socket options. Error %d: %s",errno,strerror(errno)))); } + if((syseno = setsockopt(l_so, IPPROTO_TCP, TCP_NODELAY, (char *)&one, sizeof(one))) != 0){ + LOGIF(LE, (skygw_log_write_flush(LOGFILE_ERROR,"Error: Failed to set socket options. Error %d: %s",errno,strerror(errno)))); + } // set NONBLOCKING mode setnonblocking(l_so); @@ -1605,61 +1921,79 @@ return_rc: return rc; } - /** - * Create a character array including the query string. - * GWBUF given as input includes either one complete or partial query. - * Length of buffer is at most the query length+4 (length of packet header). + * Do the SSL authentication handshake. + * This creates the DCB SSL structure if one has not been created and starts the + * SSL handshake handling. + * @param protocol Protocol to connect with SSL + * @return 1 on success, 0 when the handshake is ongoing or -1 on error */ -#if defined(NOT_USED) -static char* gw_get_or_create_querystr ( - void* data, - bool* new_allocation) +int do_ssl_accept(MySQLProtocol* protocol) { - GWBUF* buf = (GWBUF *)data; - size_t buflen; /*< first gw buffer data length */ - size_t packetlen; /*< length of mysql packet */ - size_t querylen; /*< total buffer length- */ - size_t nbytes_copied; - char* startpos; /*< first byte of query in gw buffer */ - char* str; /*< resulting query string */ - - CHK_GWBUF(buf); - packetlen = MYSQL_GET_PACKET_LEN((uint8_t *)GWBUF_DATA(buf)); - str = (char *)malloc(packetlen); /*< leave space for terminating null */ - - if (str == NULL) - { - goto return_str; - } - *new_allocation = true; - /** - * First buffer includes 4 bytes header and a type indicator byte. - */ - buflen = GWBUF_LENGTH(buf); - querylen = packetlen-1; - ss_dassert(buflen<=querylen+5); /*< 5 == header+type indicator */ - startpos = (char *)GWBUF_DATA(buf)+5; - nbytes_copied = MIN(querylen, buflen-5); - memcpy(str, startpos, nbytes_copied); - memset(&str[querylen-1], 0, 1); - buf = gwbuf_consume(buf, querylen-1); - - /** - * In case of multi-packet statement whole buffer consists of query - * string. - */ - while (buf != NULL) - { - buflen = GWBUF_LENGTH(buf); - memcpy(str+nbytes_copied, GWBUF_DATA(buf), buflen); - nbytes_copied += buflen; - buf = gwbuf_consume(buf, buflen); - } - ss_dassert(str[querylen-1] == 0); - -return_str: - return str; -} + int rval,errnum; + char errbuf[2014]; + DCB* dcb = protocol->owner_dcb; + if(dcb->ssl == NULL) + { + if(dcb_create_SSL(dcb) != 0) + { + return -1; + } + } + + rval = dcb_accept_SSL(dcb); + + switch(rval) + { + case 0: + /** Not all of the data has been read. Go back to the poll + queue and wait for more.*/ + + rval = 0; + skygw_log_write_flush(LT,"SSL_accept ongoing for %s@%s", + protocol->owner_dcb->user, + protocol->owner_dcb->remote); + return 0; + break; + case 1: + spinlock_acquire(&protocol->protocol_lock); + protocol->protocol_auth_state = MYSQL_AUTH_SSL_HANDSHAKE_DONE; + protocol->use_ssl = true; + spinlock_release(&protocol->protocol_lock); + + spinlock_acquire(&dcb->authlock); + dcb->func.write = gw_MySQLWrite_client_SSL; + dcb->func.write_ready = gw_write_client_event_SSL; + spinlock_release(&dcb->authlock); + + rval = 1; + + skygw_log_write_flush(LT,"SSL_accept done for %s@%s", + protocol->owner_dcb->user, + protocol->owner_dcb->remote); + break; + + case -1: + + spinlock_acquire(&protocol->protocol_lock); + protocol->protocol_auth_state = MYSQL_AUTH_SSL_HANDSHAKE_FAILED; + spinlock_release(&protocol->protocol_lock); + rval = -1; + skygw_log_write_flush(LE, + "Error: Fatal error in SSL_accept for %s", + protocol->owner_dcb->remote); + break; + + default: + skygw_log_write_flush(LE, + "Error: Fatal error in SSL_accept, returned value was %d.", + rval); + break; + } +#ifdef SS_DEBUG + skygw_log_write(LD,"[do_ssl_accept] Protocol state: %s", + gw_mysql_protocol_state2string(protocol->protocol_auth_state)); #endif + return rval; +} diff --git a/server/modules/protocol/mysql_common.c b/server/modules/protocol/mysql_common.c index ffd72c034..83e2912e5 100644 --- a/server/modules/protocol/mysql_common.c +++ b/server/modules/protocol/mysql_common.c @@ -44,6 +44,7 @@ #include #include #include +#include /** Defined in log_manager.cc */ extern int lm_enabled_logfiles_bitmask; @@ -137,7 +138,7 @@ void mysql_protocol_done ( goto retblock; } scmd = p->protocol_cmd_history; - + while (scmd != NULL) { scmd2 = scmd->scom_next; @@ -812,6 +813,23 @@ int gw_do_connect_to_backend( goto close_so; } + int one = 1; + if(setsockopt(so, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)) != 0) + { + LOGIF(LE, (skygw_log_write_flush( + LOGFILE_ERROR, + "Error: Failed to set socket options " + "%s:%d failed.\n\t\t Socket configuration failed " + "due %d, %s.", + host, + port, + errno, + strerror(errno)))); + rv = -1; + /** Close socket */ + goto close_so; + } + /* set socket to as non-blocking here */ setnonblocking(so); rv = connect(so, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); @@ -890,7 +908,11 @@ gw_mysql_protocol_state2string (int state) { case MYSQL_AUTH_FAILED: return "MySQL Authentication failed"; case MYSQL_IDLE: - return "MySQL authentication is succesfully done."; + return "MySQL authentication is succesfully done."; + case MYSQL_AUTH_SSL_REQ: return "MYSQL_AUTH_SSL_REQ"; + case MYSQL_AUTH_SSL_HANDSHAKE_DONE: return "MYSQL_AUTH_SSL_HANDSHAKE_DONE"; + case MYSQL_AUTH_SSL_HANDSHAKE_FAILED: return "MYSQL_AUTH_SSL_HANDSHAKE_FAILED"; + case MYSQL_AUTH_SSL_HANDSHAKE_ONGOING: return "MYSQL_AUTH_SSL_HANDSHAKE_ONGOING"; default: return "MySQL (unknown protocol state)"; } @@ -2199,7 +2221,8 @@ char *create_auth_fail_str( char *username, char *hostaddr, char *sha1, - char *db) + char *db, + int errcode) { char* errstr; const char* ferrstr; @@ -2214,6 +2237,10 @@ char *create_auth_fail_str( { ferrstr = "Access denied for user '%s'@'%s' (using password: %s) to database '%s'"; } + else if(errcode == MYSQL_FAILED_AUTH_SSL) + { + ferrstr = "Access without SSL denied"; + } else { ferrstr = "Access denied for user '%s'@'%s' (using password: %s)"; @@ -2233,6 +2260,10 @@ char *create_auth_fail_str( { sprintf(errstr, ferrstr, username, hostaddr, (*sha1 == '\0' ? "NO" : "YES"), db); } + else if(errcode == MYSQL_FAILED_AUTH_SSL) + { + sprintf(errstr, ferrstr); + } else { sprintf(errstr, ferrstr, username, hostaddr, (*sha1 == '\0' ? "NO" : "YES")); diff --git a/server/modules/protocol/test/CMakeLists.txt b/server/modules/protocol/test/CMakeLists.txt new file mode 100644 index 000000000..26653901c --- /dev/null +++ b/server/modules/protocol/test/CMakeLists.txt @@ -0,0 +1,11 @@ +configure_file(test_ssl.sh ${CMAKE_CURRENT_BINARY_DIR}/test_ssl.sh @ONLY) +configure_file(no_ca.cnf ${CMAKE_CURRENT_BINARY_DIR}/no_ca.cnf @ONLY) +configure_file(no_server_cert.cnf ${CMAKE_CURRENT_BINARY_DIR}/no_server_cert.cnf @ONLY) +configure_file(no_server_key.cnf ${CMAKE_CURRENT_BINARY_DIR}/no_server_key.cnf @ONLY) +configure_file(bad_ca.cnf ${CMAKE_CURRENT_BINARY_DIR}/bad_ca.cnf @ONLY) +configure_file(bad_cert.cnf ${CMAKE_CURRENT_BINARY_DIR}/bad_cert.cnf @ONLY) +configure_file(bad_key.cnf ${CMAKE_CURRENT_BINARY_DIR}/bad_key.cnf @ONLY) +configure_file(bad_ssl.cnf ${CMAKE_CURRENT_BINARY_DIR}/bad_ssl.cnf @ONLY) +configure_file(bad_ssl_version.cnf ${CMAKE_CURRENT_BINARY_DIR}/bad_ssl_version.cnf @ONLY) +configure_file(ok.cnf ${CMAKE_CURRENT_BINARY_DIR}/ok.cnf @ONLY) +add_test(NAME SSLTest COMMAND ${CMAKE_CURRENT_BINARY_DIR}/test_ssl.sh) diff --git a/server/modules/protocol/test/bad_ca.cnf b/server/modules/protocol/test/bad_ca.cnf new file mode 100644 index 000000000..9206679a9 --- /dev/null +++ b/server/modules/protocol/test/bad_ca.cnf @@ -0,0 +1,28 @@ +[maxscale] +threads=1 +logdir=@CMAKE_CURRENT_BINARY_DIR@ +datadir=@CMAKE_CURRENT_BINARY_DIR@ +piddir=@CMAKE_CURRENT_BINARY_DIR@ +cachedir=@CMAKE_CURRENT_BINARY_DIR@ + +[Testservice] +type=service +router=readconnroute +servers=server1 +user=user +passwd=pwd +ssl=enabled +ssl_ca_cert=This is not a value +ssl_cert=@CMAKE_CURRENT_BINARY_DIR@/server-cert +ssl_key=@CMAKE_CURRENT_BINARY_DIR@/server-key + +[Testlistener] +type=listener +service=Testservice +protocol=MySQLBackend +port=12345 + +[server1] +type=server +address=127.0.0.1 +port=4321 diff --git a/server/modules/protocol/test/bad_cert.cnf b/server/modules/protocol/test/bad_cert.cnf new file mode 100644 index 000000000..1b4c776cc --- /dev/null +++ b/server/modules/protocol/test/bad_cert.cnf @@ -0,0 +1,28 @@ +[maxscale] +threads=1 +logdir=@CMAKE_CURRENT_BINARY_DIR@ +datadir=@CMAKE_CURRENT_BINARY_DIR@ +piddir=@CMAKE_CURRENT_BINARY_DIR@ +cachedir=@CMAKE_CURRENT_BINARY_DIR@ + +[Testservice] +type=service +router=readconnroute +servers=server1 +user=user +passwd=pwd +ssl=enabled +ssl_ca_cert=@CMAKE_CURRENT_BINARY_DIR@/ca +ssl_cert=This is not a value +ssl_key=@CMAKE_CURRENT_BINARY_DIR@/server-key + +[Testlistener] +type=listener +service=Testservice +protocol=MySQLBackend +port=12345 + +[server1] +type=server +address=127.0.0.1 +port=4321 diff --git a/server/modules/protocol/test/bad_key.cnf b/server/modules/protocol/test/bad_key.cnf new file mode 100644 index 000000000..4e0be5f05 --- /dev/null +++ b/server/modules/protocol/test/bad_key.cnf @@ -0,0 +1,28 @@ +[maxscale] +threads=1 +logdir=@CMAKE_CURRENT_BINARY_DIR@ +datadir=@CMAKE_CURRENT_BINARY_DIR@ +piddir=@CMAKE_CURRENT_BINARY_DIR@ +cachedir=@CMAKE_CURRENT_BINARY_DIR@ + +[Testservice] +type=service +router=readconnroute +servers=server1 +user=user +passwd=pwd +ssl=enabled +ssl_ca_cert=@CMAKE_CURRENT_BINARY_DIR@/ca +ssl_cert=@CMAKE_CURRENT_BINARY_DIR@/server-cert +ssl_key=This is not a value + +[Testlistener] +type=listener +service=Testservice +protocol=MySQLBackend +port=12345 + +[server1] +type=server +address=127.0.0.1 +port=4321 diff --git a/server/modules/protocol/test/bad_ssl.cnf b/server/modules/protocol/test/bad_ssl.cnf new file mode 100644 index 000000000..f6dcff1a1 --- /dev/null +++ b/server/modules/protocol/test/bad_ssl.cnf @@ -0,0 +1,28 @@ +[maxscale] +threads=1 +logdir=@CMAKE_CURRENT_BINARY_DIR@ +datadir=@CMAKE_CURRENT_BINARY_DIR@ +piddir=@CMAKE_CURRENT_BINARY_DIR@ +cachedir=@CMAKE_CURRENT_BINARY_DIR@ + +[Testservice] +type=service +router=readconnroute +servers=server1 +user=user +passwd=pwd +ssl=testing +ssl_ca_cert=@CMAKE_CURRENT_BINARY_DIR@/ca +ssl_cert=@CMAKE_CURRENT_BINARY_DIR@/server-cert +ssl_key=@CMAKE_CURRENT_BINARY_DIR@/server-key + +[Testlistener] +type=listener +service=Testservice +protocol=MySQLBackend +port=12345 + +[server1] +type=server +address=127.0.0.1 +port=4321 diff --git a/server/modules/protocol/test/bad_ssl_version.cnf b/server/modules/protocol/test/bad_ssl_version.cnf new file mode 100644 index 000000000..6849b1904 --- /dev/null +++ b/server/modules/protocol/test/bad_ssl_version.cnf @@ -0,0 +1,29 @@ +[maxscale] +threads=1 +logdir=@CMAKE_CURRENT_BINARY_DIR@ +datadir=@CMAKE_CURRENT_BINARY_DIR@ +piddir=@CMAKE_CURRENT_BINARY_DIR@ +cachedir=@CMAKE_CURRENT_BINARY_DIR@ + +[Testservice] +type=service +router=readconnroute +servers=server1 +user=user +passwd=pwd +ssl=enabled +ssl_ca_cert=@CMAKE_CURRENT_BINARY_DIR@/ca +ssl_cert=@CMAKE_CURRENT_BINARY_DIR@/server-cert +ssl_key=@CMAKE_CURRENT_BINARY_DIR@/server-key +ssl_version=Don't use SSL, it's not needed! + +[Testlistener] +type=listener +service=Testservice +protocol=MySQLBackend +port=12345 + +[server1] +type=server +address=127.0.0.1 +port=4321 diff --git a/server/modules/protocol/test/no_ca.cnf b/server/modules/protocol/test/no_ca.cnf new file mode 100644 index 000000000..56f603f6a --- /dev/null +++ b/server/modules/protocol/test/no_ca.cnf @@ -0,0 +1,28 @@ +[maxscale] +threads=1 +logdir=@CMAKE_CURRENT_BINARY_DIR@ +datadir=@CMAKE_CURRENT_BINARY_DIR@ +piddir=@CMAKE_CURRENT_BINARY_DIR@ +cachedir=@CMAKE_CURRENT_BINARY_DIR@ + +[Testservice] +type=service +router=readconnroute +servers=server1 +user=user +passwd=pwd +ssl=enabled +#ssl_ca_cert=@CMAKE_CURRENT_BINARY_DIR@/ca +ssl_cert=@CMAKE_CURRENT_BINARY_DIR@/server-cert +ssl_key=@CMAKE_CURRENT_BINARY_DIR@/server-key + +[Testlistener] +type=listener +service=Testservice +protocol=MySQLBackend +port=12345 + +[server1] +type=server +address=127.0.0.1 +port=4321 diff --git a/server/modules/protocol/test/no_server_cert.cnf b/server/modules/protocol/test/no_server_cert.cnf new file mode 100644 index 000000000..f714a0b3f --- /dev/null +++ b/server/modules/protocol/test/no_server_cert.cnf @@ -0,0 +1,28 @@ +[maxscale] +threads=1 +logdir=@CMAKE_CURRENT_BINARY_DIR@ +datadir=@CMAKE_CURRENT_BINARY_DIR@ +piddir=@CMAKE_CURRENT_BINARY_DIR@ +cachedir=@CMAKE_CURRENT_BINARY_DIR@ + +[Testservice] +type=service +router=readconnroute +servers=server1 +user=user +passwd=pwd +ssl=enabled +ssl_ca_cert=@CMAKE_CURRENT_BINARY_DIR@/ca +#ssl_cert=@CMAKE_CURRENT_BINARY_DIR@/server-cert +ssl_key=@CMAKE_CURRENT_BINARY_DIR@/server-key + +[Testlistener] +type=listener +service=Testservice +protocol=MySQLBackend +port=12345 + +[server1] +type=server +address=127.0.0.1 +port=4321 diff --git a/server/modules/protocol/test/no_server_key.cnf b/server/modules/protocol/test/no_server_key.cnf new file mode 100644 index 000000000..a820ee414 --- /dev/null +++ b/server/modules/protocol/test/no_server_key.cnf @@ -0,0 +1,28 @@ +[maxscale] +threads=1 +logdir=@CMAKE_CURRENT_BINARY_DIR@ +datadir=@CMAKE_CURRENT_BINARY_DIR@ +piddir=@CMAKE_CURRENT_BINARY_DIR@ +cachedir=@CMAKE_CURRENT_BINARY_DIR@ + +[Testservice] +type=service +router=readconnroute +servers=server1 +user=user +passwd=pwd +ssl=enabled +ssl_ca_cert=@CMAKE_CURRENT_BINARY_DIR@/ca +ssl_cert=@CMAKE_CURRENT_BINARY_DIR@/server-cert +#ssl_key=@CMAKE_CURRENT_BINARY_DIR@/server-key + +[Testlistener] +type=listener +service=Testservice +protocol=MySQLBackend +port=12345 + +[server1] +type=server +address=127.0.0.1 +port=4321 diff --git a/server/modules/protocol/test/ok.cnf b/server/modules/protocol/test/ok.cnf new file mode 100644 index 000000000..089025c0d --- /dev/null +++ b/server/modules/protocol/test/ok.cnf @@ -0,0 +1,28 @@ +[maxscale] +threads=1 +logdir=@CMAKE_CURRENT_BINARY_DIR@ +datadir=@CMAKE_CURRENT_BINARY_DIR@ +piddir=@CMAKE_CURRENT_BINARY_DIR@ +cachedir=@CMAKE_CURRENT_BINARY_DIR@ + +[Testservice] +type=service +router=readconnroute +servers=server1 +user=user +passwd=pwd +ssl=enabled +ssl_ca_cert=@CMAKE_CURRENT_BINARY_DIR@/ca +ssl_cert=@CMAKE_CURRENT_BINARY_DIR@/server-cert +ssl_key=@CMAKE_CURRENT_BINARY_DIR@/server-key + +[Testlistener] +type=listener +service=Testservice +protocol=MySQLBackend +port=12345 + +[server1] +type=server +address=127.0.0.1 +port=4321 diff --git a/server/modules/protocol/test/test_ssl.sh b/server/modules/protocol/test/test_ssl.sh new file mode 100755 index 000000000..f14670689 --- /dev/null +++ b/server/modules/protocol/test/test_ssl.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash + +function create_certs() +{ + echo "CA cert" > @CMAKE_CURRENT_BINARY_DIR@/ca.pem + echo "Server Certificate" > @CMAKE_CURRENT_BINARY_DIR@/server-cert.pem + echo "Server Key" > @CMAKE_CURRENT_BINARY_DIR@/server-key.pem +} + +function start_maxscale () +{ + local result=$(@CMAKE_INSTALL_PREFIX@/@MAXSCALE_BINDIR@/maxscale -d -f $1 &> $1.log;echo $?) + if [[ $result == "0" ]] + then + echo "Error: $1 exited with status $result!" + exit 1 + fi +} + +# All test cases expect that MaxScale will not start with a bad configuration or missing certificates + +# No CA defined +printf "Testing No CA defined" +start_maxscale @CMAKE_CURRENT_BINARY_DIR@/no_ca.cnf +echo " OK" + +# No cert defined +printf "Testing No cert defined" +start_maxscale @CMAKE_CURRENT_BINARY_DIR@/no_cert.cnf +echo " OK" + +# No key defined +printf "Testing No key defined" +start_maxscale @CMAKE_CURRENT_BINARY_DIR@/no_key.cnf +echo " OK" + +# Bad SSL value defined +printf "Testing Bad SSL defined" +start_maxscale @CMAKE_CURRENT_BINARY_DIR@/bad_ssl.cnf +echo " OK" + +# Bad SSL version defined +printf "Testing Bad SSL version defined" +start_maxscale @CMAKE_CURRENT_BINARY_DIR@/bad_ssl_version.cnf +echo " OK" + +# Bad CA value defined +printf "Testing Bad CA defined" +start_maxscale @CMAKE_CURRENT_BINARY_DIR@/bad_ca.cnf +echo " OK" + +# Bad server certificate defined +printf "Testing Bad cert defined" +start_maxscale @CMAKE_CURRENT_BINARY_DIR@/bad_cert.cnf +echo " OK" + +# Bad server key defined +printf "Testing Bad key defined" +start_maxscale @CMAKE_CURRENT_BINARY_DIR@/bad_key.cnf +echo " OK" + +# No CA file +printf "Testing No CA file" +create_certs +rm @CMAKE_CURRENT_BINARY_DIR@/ca.pem +start_maxscale @CMAKE_CURRENT_BINARY_DIR@/ok.cnf +echo " OK" + +# No server certificate file +printf "Testing No cert file" +create_certs +rm @CMAKE_CURRENT_BINARY_DIR@/server-cert.pem +start_maxscale @CMAKE_CURRENT_BINARY_DIR@/ok.cnf +echo " OK" + +# No server key file +printf "Testing No key file" +create_certs +rm @CMAKE_CURRENT_BINARY_DIR@/server-key.pem +start_maxscale @CMAKE_CURRENT_BINARY_DIR@/ok.cnf +echo " OK" + +exit 0