view usr/src/lib/smbsrv/libsmbrdr/common/smbrdr_session.c @ 13328:2f33da224406

849 domain controller "hot fail over" can take forever Reviewed by: Albert Lee <trisk@nexenta.com> Reviewed by: Garrett D'Amore <garrett@nexenta.com> Reviewed by: Richard Lowe <richlowe@richlowe.net> Approved by: Richard Lowe <richlowe@richlowe.net>
author Gordon Ross <gwr@nexenta.com>
date Thu, 07 Apr 2011 19:44:19 -0400
parents edb7861a1533
children e49c34fd7751
line wrap: on
line source

/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright (c) 2007, 2010, Oracle and/or its affiliates. All rights reserved.
 * Copyright 2011 Nexenta Systems, Inc.  All rights reserved.
 */

/*
 * This module provides the netbios and SMB negotiation, connect and
 * disconnect interface.
 */

#include <unistd.h>
#include <synch.h>
#include <string.h>
#include <strings.h>
#include <pthread.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <inttypes.h>
#include <netdb.h>

#include <smbsrv/libsmbrdr.h>
#include <smbsrv/netbios.h>
#include <smbrdr.h>

#define	SMBRDR_DOMAIN_MAX		32

static uint16_t smbrdr_ports[] = {
	IPPORT_SMB,
	IPPORT_NETBIOS_SSN
};

static int smbrdr_nports = sizeof (smbrdr_ports) / sizeof (smbrdr_ports[0]);

static struct sdb_session session_table[SMBRDR_DOMAIN_MAX];
static mutex_t smbrdr_screate_mtx;
static uint32_t session_id = 0;

static struct sdb_session *smbrdr_session_init(char *, char *);
static int smbrdr_trnsprt_connect(struct sdb_session *, uint16_t);
static int smbrdr_session_connect(char *, char *);
static int smbrdr_smb_negotiate(struct sdb_session *);
static int smbrdr_smb_echo(struct sdb_session *);
static void smbrdr_session_disconnect(struct sdb_session *, int);


static void
smbrdr_session_clear(struct sdb_session *session)
{
	bzero(session, sizeof (struct sdb_session) - sizeof (rwlock_t));
}

/*
 * Disconnects the session with given server.
 */
void
smbrdr_disconnect(const char *server)
{
	struct sdb_session *session;

	session = smbrdr_session_lock(server, SDB_SLCK_WRITE);
	if (session) {
		smbrdr_session_disconnect(session, 0);
		smbrdr_session_unlock(session);
	}
}

/*
 * smbrdr_negotiate
 *
 * Negotiate a session with a domain controller in the specified domain.
 * The domain must be one of values from the smbinfo that indicates the
 * resource domain or the account domain.
 *
 * If a session already exists, we can use that one. Otherwise we create
 * a new one. This sets up the session key and session security info that
 * we'll need later to authenticate the user. The session security info
 * is returned to support the SMB client pass-through authentication
 * interface.
 *
 * Returns 0 on success, otherwise -1.
 */
/*ARGSUSED*/
int
smbrdr_negotiate(char *domain_controller, char *domain)
{
	struct sdb_session *session = 0;
	int rc;

	/*
	 * The mutex is to make session lookup and create atomic
	 * so we don't end up with two sessions with the same
	 * server.
	 */
	(void) mutex_lock(&smbrdr_screate_mtx);
	session = smbrdr_session_lock(domain_controller, SDB_SLCK_WRITE);
	if (session != 0) {
		if (nb_keep_alive(session->sock, session->port) == 0) {
			/* session is good, use it */
			smbrdr_session_unlock(session);
			rc = 0;
			goto done;

		} else {
			/* stale session */
			smbrdr_session_clear(session);
			smbrdr_session_unlock(session);
		}
	}

	rc = smbrdr_session_connect(domain_controller, domain);
done:
	(void) mutex_unlock(&smbrdr_screate_mtx);

	if (rc != 0)
		smb_log(smbrdr_log_hdl, LOG_DEBUG,
		    "smbrdr_negotiate: cannot access domain");
	return (rc);
}

/*
 * smbrdr_session_connect
 *
 * This is the entry point for establishing an SMB connection to a
 * domain controller. A session structure is allocated, a netbios
 * session is set up and the SMB protocol is negotiated. If this is
 * successful, the returned session structure can be used to logon
 * to the the domain. A null pointer is returned if the connect fails.
 */
static int
smbrdr_session_connect(char *domain_controller, char *domain)
{
	struct sdb_session *session;
	uint16_t port;
	int rc = 0;

	/*
	 * smbrdr_session_init() will lock the session so that it wouldn't
	 * be accessible until it's established otherwise another thread
	 * might get access to a session which is not fully established.
	 */
	if ((session = smbrdr_session_init(domain_controller, domain))
	    == NULL) {
		smb_log(smbrdr_log_hdl, LOG_DEBUG,
		    "smbrdr_session_init failed");
		return (-1);
	}

	for (port = 0; port < smbrdr_nports; ++port) {
		rc = smbrdr_trnsprt_connect(session, smbrdr_ports[port]);
		if (rc == 0)
			break;
	}

	if (rc < 0) {
		smbrdr_session_clear(session);
		smbrdr_session_unlock(session);
		smb_log(smbrdr_log_hdl, LOG_DEBUG,
		    "smbrdr_session_connect: connect failed");
		return (-1);
	}

	if (smbrdr_smb_negotiate(session) < 0) {
		(void) close(session->sock);
		smbrdr_session_clear(session);
		smbrdr_session_unlock(session);
		smb_log(smbrdr_log_hdl, LOG_DEBUG,
		    "smbrdr_session_connect: negotiate failed");
		return (-1);
	}

	smbrdr_session_unlock(session);
	return (0);
}


/*
 * smbrdr_trnsprt_connect
 *
 * Set up the TCP/IP and NETBIOS protocols for a session. This is just
 * standard socket sutff. The paranoia check for socket descriptor 0
 * is because we had a problem with this value and the console telnet
 * interface will lock up if we use and/or close stdin (0).
 *
 * Return 0 on success. Otherwise return (-1) to indicate a problem.
 */
static int
smbrdr_trnsprt_connect(struct sdb_session *sess, uint16_t port)
{
	char hostname[MAXHOSTNAMELEN];
	struct sockaddr_in sin;
	struct sockaddr_in6 sin6;
	int sock, rc, tmo;
	smb_wchar_t unicode_server_name[SMB_PI_MAX_DOMAIN];
	char server_name[SMB_PI_MAX_DOMAIN];
	char ipstr[INET6_ADDRSTRLEN];

	if ((sock = socket(sess->srv_ipaddr.a_family, SOCK_STREAM, 0)) <= 0) {
		smb_log(smbrdr_log_hdl, LOG_ERR,
		    "smbrdr_trnsprt_connect: socket failed: %s",
		    strerror(errno));
		return (-1);
	}

	/*
	 * The default connect timeout is very long.
	 * We want to give up after 45 sec.
	 */
	tmo = 1000 * smbrdr_default_timeout;
	setsockopt(sock, IPPROTO_TCP, TCP_CONN_ABORT_THRESHOLD,
	    &tmo, sizeof (tmo));

	if (sess->srv_ipaddr.a_family == AF_INET) {
		bzero(&sin, sizeof (struct sockaddr_in));
		sin.sin_family = AF_INET;
		sin.sin_addr.s_addr = sess->srv_ipaddr.a_ipv4;
		sin.sin_port = htons(port);
		rc = connect(sock, (struct sockaddr *)&sin, sizeof (sin));
	} else {
		(void) smb_inet_ntop(&sess->srv_ipaddr, ipstr,
		    SMB_IPSTRLEN(sess->srv_ipaddr.a_family));
		bzero(&sin6, sizeof (struct sockaddr_in6));
		sin6.sin6_family = AF_INET6;
		bcopy(&sess->srv_ipaddr.a_ipv6, &sin6.sin6_addr.s6_addr,
		    IPV6_ADDR_LEN);
		sin6.sin6_port = htons(port);
		rc = connect(sock, (struct sockaddr *)&sin6, sizeof (sin6));
	}

	if (rc  < 0) {
		smb_log(smbrdr_log_hdl, LOG_ERR,
		    "smbrdr_trnsprt_connect: connect failed: %s",
		    strerror(errno));
		if (sock != 0)
			(void) close(sock);
		return (-1);
	}

	(void) smb_mbstowcs(unicode_server_name, sess->srv_name,
	    SMB_PI_MAX_DOMAIN);
	rc = ucstooem(server_name, unicode_server_name, SMB_PI_MAX_DOMAIN,
	    OEM_CPG_850);
	if (rc == 0) {
		smb_log(smbrdr_log_hdl, LOG_ERR,
		    "smbrdr_trnsprt_connect: unicode conversion failed");
		if (sock != 0)
			(void) close(sock);
		return (-1);
	}

	/*
	 * If we are using NetBIOS, we need to set up a NETBIOS session.
	 * This typically implies that we will be using port 139.
	 * Otherwise, we're doing NetBIOS-less SMB, i.e. SMB over TCP,
	 * which is typically on port 445.
	 */
	if (port == IPPORT_NETBIOS_SSN) {
		if (smb_getnetbiosname(hostname, MAXHOSTNAMELEN) != 0) {
			smb_log(smbrdr_log_hdl, LOG_ERR,
			    "smbrdr_trnsprt_connect: no hostname");
			if (sock != 0)
				(void) close(sock);
			return (-1);
		}

		rc = nb_session_request(sock,
		    server_name, sess->scope, hostname, sess->scope);

		if (rc != 0) {
			smb_log(smbrdr_log_hdl, LOG_ERR,
			    "smbrdr_trnsprt_connect: NBT session request "
			    "to %s failed %d", server_name, rc);
			if (sock != 0)
				(void) close(sock);
			return (-1);
		}
	}

	sess->sock = sock;
	sess->port = port;
	smb_log(smbrdr_log_hdl, LOG_DEBUG,
	    "smbrdr_trnsprt_connect: connected on port %d", port);
	sess->state = SDB_SSTATE_CONNECTED;
	return (0);
}

/*
 * smbrdr_smb_negotiate
 *
 * Negotiate the protocol we are going to use as described in CIFS
 * section 4.1.1. The only protocol we support is NT LM 0.12, so we
 * really expect to see dialect 0 in the response. The only other
 * data gathered is the session key.
 *
 * Negotiate using ASCII strings.
 *
 * Return 0 on success. Otherwise return a -ve error code.
 */
static int
smbrdr_smb_negotiate(struct sdb_session *sess)
{
	unsigned short dialect;
	smbrdr_handle_t srh;
	smb_hdr_t smb_hdr;
	smb_msgbuf_t *mb;
	DWORD status;
	int rc;
	uint8_t tmp_secmode;
	uint8_t tmp_clen;

	status = smbrdr_request_init(&srh, SMB_COM_NEGOTIATE, sess, 0, 0);

	if (status != NT_STATUS_SUCCESS)
		return (-1);

	mb = &srh.srh_mbuf;
	rc = smb_msgbuf_encode(mb, "bwbs", 0, 12, 0x02, "NT LM 0.12");
	if (rc <= 0) {
		smbrdr_handle_free(&srh);
		return (-1);
	}

	status = smbrdr_exchange(&srh, &smb_hdr, 0);
	if (status != NT_STATUS_SUCCESS) {
		smb_log(smbrdr_log_hdl, LOG_DEBUG, "smbrdr_smb_negotiate: %s",
		    xlate_nt_status(status));
		smbrdr_handle_free(&srh);
		return (-1);
	}

	sess->secmode = 0;
	sess->sesskey = 0;
	sess->challenge_len = 0;

	rc = smb_msgbuf_decode(mb,
	    "1.(dialect)w(mode)b12.(key)l(cap)l10.(keylen)b2.",
	    &dialect, &tmp_secmode, &sess->sesskey, &sess->remote_caps,
	    &tmp_clen);

	if (rc <= 0 || dialect != 0) {
		smbrdr_handle_free(&srh);
		return (-1);
	}
	sess->secmode = tmp_secmode;
	sess->challenge_len = tmp_clen;

	rc = smb_msgbuf_decode(mb, "#c",
	    sess->challenge_len, sess->challenge_key);
	if (rc <= 0) {
		smbrdr_handle_free(&srh);
		return (-1);
	}

	smbrdr_handle_free(&srh);

	if ((sess->secmode & NEGOTIATE_SECURITY_SIGNATURES_REQUIRED) &&
	    (sess->secmode & NEGOTIATE_SECURITY_SIGNATURES_ENABLED)) {
		sess->sign_ctx.ssc_flags |= SMB_SCF_REQUIRED;
		smb_log(smbrdr_log_hdl, LOG_DEBUG,
		    "smbrdr_smb_negotiate: %s: signing required",
		    sess->srv_name);
	}

	sess->state = SDB_SSTATE_NEGOTIATED;
	return (0);
}

/*
 * smbrdr_session_init
 *
 * Allocate an available slot in session table for the specified domain
 * information.
 *
 * IMPORTANT! the returned session will be locked caller has to unlock
 *            it by calling smbrdr_session_unlock() after it's done with
 *            the pointer.
 */
static struct sdb_session *
smbrdr_session_init(char *domain_controller, char *domain)
{
	struct sdb_session *session = NULL;
	smb_inaddr_t ipaddr;
	int i, rc;
	struct hostent *h;

	if (domain_controller == NULL || domain == NULL)
		return (NULL);

	if ((h = smb_gethostbyname(domain_controller, &rc)) == NULL) {
		smb_log(smbrdr_log_hdl, LOG_DEBUG,
		    "smbrdr_session_init: failed to resolve %s to IP (%d)",
		    domain_controller, rc);
		return (NULL);
	}

	(void) memcpy(&ipaddr, h->h_addr, h->h_length);
	ipaddr.a_family = h->h_addrtype;
	freehostent(h);

	for (i = 0; i < SMBRDR_DOMAIN_MAX; ++i) {
		session = &session_table[i];

		(void) rw_wrlock(&session->rwl);
		if (session->state == SDB_SSTATE_START) {
			smbrdr_session_clear(session);
			(void) strlcpy(session->srv_name, domain_controller,
			    MAXHOSTNAMELEN);
			(void) smb_strupr(session->srv_name);

			session->srv_ipaddr = ipaddr;
			(void) strlcpy(session->domain, domain, MAXHOSTNAMELEN);
			(void) smb_strupr(session->domain);

			(void) smb_config_getstr(SMB_CI_NBSCOPE, session->scope,
			    sizeof (session->scope));

			(void) strlcpy(session->native_os,
			    "Solaris", SMB_PI_MAX_NATIVE_OS);
			(void) strlcpy(session->native_lanman,
			    "Windows NT 4.0", SMB_PI_MAX_LANMAN);
			session->sock = -1;
			session->port = smbrdr_ports[0];
			session->smb_flags = SMB_FLAGS_CANONICALIZED_PATHS
			    | SMB_FLAGS_CASE_INSENSITIVE;

			session->smb_flags2 = SMB_FLAGS2_KNOWS_LONG_NAMES
			    | SMB_FLAGS2_KNOWS_EAS;

			/*
			 * Note that by sending vc=0 server will shutdown all
			 * the other connections with NAS if there is any.
			 */
			session->vc = 0;
			session->sid = ++session_id;
			if (session->sid == 0)
				session->sid = 1;
			session->state = SDB_SSTATE_INIT;
			return (session);
		}
		(void) rw_unlock(&session->rwl);
	}

	smb_log(smbrdr_log_hdl, LOG_DEBUG,
	    "smbrdr_session_init: no session available");
	return (NULL);
}

/*
 * smbrdr_session_disconnect
 *
 * This is the entry point for disconnecting an SMB connection. Ensure
 * that all logons and shares associated with this session are
 * terminated and then free the session.
 *
 * if 'cleanup' is 1 it means that only sessions that are not active
 * should be cleaned up. if 'cleanup' is 0 disconnect the session in any
 * states.
 */
static void
smbrdr_session_disconnect(struct sdb_session *session, int cleanup)
{
	int state;

	if (session == NULL)
		return;

	state = session->state;
	if ((state != SDB_SSTATE_DISCONNECTING) &&
	    (state != SDB_SSTATE_CLEANING) &&
	    (state != SDB_SSTATE_START)) {
		if ((cleanup == 0) || (state == SDB_SSTATE_STALE)) {
			/*
			 * if session is in stale state it means the connection
			 * is lost so no logoff, tdcon, or close can actually
			 * be sent, thus only cleanup our side.
			 */
			session->state = (state == SDB_SSTATE_STALE)
			    ? SDB_SSTATE_CLEANING : SDB_SSTATE_DISCONNECTING;
			(void) smbrdr_logoffx(&session->logon);
			nb_close(session->sock);
			smbrdr_session_clear(session);
		}
	}
}

/*
 * smbrdr_session_unlock
 *
 * Unlock given session structure.
 */
void
smbrdr_session_unlock(struct sdb_session *session)
{
	if (session)
		(void) rw_unlock(&session->rwl);
}

/*
 * smbrdr_session_lock
 *
 * Lookup the session associated with the specified domain controller.
 * If a match is found, we return a pointer to the session, Otherwise
 * we return null. Only sessions in "negotiated" state are checked.
 * This mechanism is very simple and implies that we
 * should only ever have one session open to any domain controller.
 *
 * IMPORTANT! the returned session will be locked caller has to unlock
 *            it by calling smbrdr_session_unlock() after it's done with
 *            the pointer.
 */
struct sdb_session *
smbrdr_session_lock(const char *server, int lmode)
{
	struct sdb_session *session;
	int i;

	if (server == NULL)
		return (NULL);

	for (i = 0; i < SMBRDR_DOMAIN_MAX; ++i) {
		session = &session_table[i];

		(lmode == SDB_SLCK_READ) ? (void) rw_rdlock(&session->rwl) :
		    (void) rw_wrlock(&session->rwl);

		if (session->state == SDB_SSTATE_STALE) {
			smbrdr_session_clear(session);
			(void) rw_unlock(&session->rwl);
			continue;
		}

		if ((session->state == SDB_SSTATE_NEGOTIATED) &&
		    (smb_strcasecmp(session->srv_name, server, 0) == 0)) {
			return (session);
		}

		(void) rw_unlock(&session->rwl);
	}

	return (NULL);
}

/*
 * smbrdr_dump_sessions
 *
 * Debug function to dump the session table.
 */
void
smbrdr_dump_sessions(void)
{
	struct sdb_session *session;
	struct sdb_logon *logon;
	char ipstr[INET6_ADDRSTRLEN];
	int i;

	for (i = 0; i < SMBRDR_DOMAIN_MAX; ++i) {
		session = &session_table[i];

		(void) rw_rdlock(&session->rwl);
		if (session->state != SDB_SSTATE_START) {
			(void) smb_inet_ntop(&session->srv_ipaddr, ipstr,
			    SMB_IPSTRLEN(session->srv_ipaddr.a_family));
			smb_log(smbrdr_log_hdl, LOG_DEBUG,
			    "smbrdr_dump_sessions: session[%d]: state=%d",
			    i, session->state);
			smb_log(smbrdr_log_hdl, LOG_DEBUG,
			    "smbrdr_dump_sessions: session[%d]: %s %s (%s)", i,
			    session->domain, session->srv_name, ipstr);
			smb_log(smbrdr_log_hdl, LOG_DEBUG,
			    "smbrdr_dump_sessions: session[%d]: %s %s "
			    "(sock=%d)", i,
			    session->native_os, session->native_lanman,
			    session->sock);

			logon = &session->logon;
			if (logon->type != SDB_LOGON_NONE)
				smb_log(smbrdr_log_hdl, LOG_DEBUG,
				    "smbrdr_dump_sessions: logon[%d]: %s "
				    "(uid=%d)",
				    i, logon->username, logon->uid);
		}
		(void) rw_unlock(&session->rwl);
	}
}

int
smbrdr_echo(const char *server)
{
	struct sdb_session *session;
	int res = 0;

	if ((session = smbrdr_session_lock(server, SDB_SLCK_WRITE)) != NULL) {
		res = smbrdr_smb_echo(session);
		smbrdr_session_unlock(session);
	}

	return (res);
}

/*
 * This request can be used to test the connection to the server. The
 * server should echo the data sent. The server should ignore the tid
 * in the header, so this request when there are no tree connections.
 * See CIFS/1.0 section 4.1.7.
 *
 * Return 0 on success. Otherwise return a -ve error code.
 */
static int
smbrdr_smb_echo(struct sdb_session *session)
{
	static char *echo_str = "smbrdr";
	smbrdr_handle_t srh;
	smb_hdr_t smb_hdr;
	DWORD status;
	int rc;

	if ((session->state == SDB_SSTATE_DISCONNECTING) ||
	    (session->state == SDB_SSTATE_CLEANING) ||
	    (session->state == SDB_SSTATE_STALE)) {
		return (-1);
	}

	status = smbrdr_request_init(&srh, SMB_COM_ECHO, session, 0, 0);
	if (status != NT_STATUS_SUCCESS)
		return (-1);

	rc = smb_msgbuf_encode(&srh.srh_mbuf, "bwws", 1, 1,
	    strlen(echo_str), echo_str);
	if (rc <= 0) {
		smbrdr_handle_free(&srh);
		return (-1);
	}

	status = smbrdr_exchange(&srh, &smb_hdr, 10);
	smbrdr_handle_free(&srh);

	if (status != NT_STATUS_SUCCESS)
		return (-1);

	return (0);
}