HADOOP-6706. Improves the sasl failure handling due to expired tickets, and other server detected failures. Contributed by Jitendra Pandey and Devaraj Das.

git-svn-id: https://svn.apache.org/repos/asf/hadoop/common/trunk@981714 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Devaraj Das 2010-08-03 00:51:57 +00:00
parent 1035138b4c
commit c47d34a866
3 changed files with 186 additions and 112 deletions

View File

@ -173,6 +173,9 @@ Trunk (unreleased changes)
HADOOP-6873. using delegation token over hftp for long HADOOP-6873. using delegation token over hftp for long
running clients (boryas) running clients (boryas)
HADOOP-6706. Improves the sasl failure handling due to expired tickets,
and other server detected failures. (Jitendra Pandey and ddas via ddas)
Release 0.21.0 - Unreleased Release 0.21.0 - Unreleased
INCOMPATIBLE CHANGES INCOMPATIBLE CHANGES

View File

@ -36,6 +36,7 @@
import java.security.PrivilegedExceptionAction; import java.security.PrivilegedExceptionAction;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.Iterator; import java.util.Iterator;
import java.util.Random;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
@ -367,53 +368,109 @@ private synchronized void disposeSasl() {
if (saslRpcClient != null) { if (saslRpcClient != null) {
try { try {
saslRpcClient.dispose(); saslRpcClient.dispose();
saslRpcClient = null;
} catch (IOException ignored) { } catch (IOException ignored) {
} }
} }
} }
private synchronized boolean shouldAuthenticateOverKrb() throws IOException {
UserGroupInformation loginUser = UserGroupInformation.getLoginUser();
UserGroupInformation currentUser = UserGroupInformation.getCurrentUser();
UserGroupInformation realUser = currentUser.getRealUser();
if (authMethod == AuthMethod.KERBEROS && loginUser != null &&
// Make sure user logged in using Kerberos either keytab or TGT
loginUser.hasKerberosCredentials() &&
// relogin only in case it is the login user (e.g. JT)
// or superuser (like oozie).
(loginUser.equals(currentUser) || loginUser.equals(realUser))) {
return true;
}
return false;
}
private synchronized boolean setupSaslConnection(final InputStream in2, private synchronized boolean setupSaslConnection(final InputStream in2,
final OutputStream out2) final OutputStream out2)
throws IOException { throws IOException {
try { saslRpcClient = new SaslRpcClient(authMethod, token, serverPrincipal);
saslRpcClient = new SaslRpcClient(authMethod, token, return saslRpcClient.saslConnect(in2, out2);
serverPrincipal); }
return saslRpcClient.saslConnect(in2, out2);
} catch (javax.security.sasl.SaslException je) { private synchronized void setupConnection() throws IOException {
UserGroupInformation loginUser = UserGroupInformation.getLoginUser(); short ioFailures = 0;
UserGroupInformation currentUser = short timeoutFailures = 0;
UserGroupInformation.getCurrentUser(); while (true) {
UserGroupInformation realUser = currentUser.getRealUser(); try {
if (authMethod == AuthMethod.KERBEROS && this.socket = socketFactory.createSocket();
//try setting up the connection again this.socket.setTcpNoDelay(tcpNoDelay);
// relogin only in case it is the login user (e.g. JT) // connection time out is 20s
// or superuser (like oozie). NetUtils.connect(this.socket, remoteId.getAddress(), 20000);
((currentUser != null && currentUser.equals(loginUser)) || this.socket.setSoTimeout(pingInterval);
(realUser != null && realUser.equals(loginUser)))) { return;
try { } catch (SocketTimeoutException toe) {
//try re-login /*
if (UserGroupInformation.isLoginKeytabBased()) { * The max number of retries is 45, which amounts to 20s*45 = 15
loginUser.reloginFromKeytab(); * minutes retries.
} else { */
loginUser.reloginFromTicketCache(); handleConnectionFailure(timeoutFailures++, 45, toe);
} } catch (IOException ie) {
disposeSasl(); handleConnectionFailure(ioFailures++, maxRetries, ie);
saslRpcClient = new SaslRpcClient(authMethod, token, }
serverPrincipal);
return saslRpcClient.saslConnect(in2, out2);
} catch (javax.security.sasl.SaslException jee) {
LOG.warn("Couldn't setup connection for " +
loginUser.getUserName() +
" to " + serverPrincipal + " even after relogin.");
throw jee;
} catch (IOException ie) {
ie.initCause(je);
throw ie;
}
}
throw je;
} }
} }
/**
* If multiple clients with the same principal try to connect to the same
* server at the same time, the server assumes a replay attack is in
* progress. This is a feature of kerberos. In order to work around this,
* what is done is that the client backs off randomly and tries to initiate
* the connection again. The other problem is to do with ticket expiry. To
* handle that, a relogin is attempted.
*/
private synchronized void handleSaslConnectionFailure(
final int currRetries, final int maxRetries, final Exception ex,
final Random rand, final UserGroupInformation ugi) throws IOException,
InterruptedException {
ugi.doAs(new PrivilegedExceptionAction<Object>() {
public Object run() throws IOException, InterruptedException {
final short MAX_BACKOFF = 5000;
closeConnection();
disposeSasl();
if (shouldAuthenticateOverKrb()) {
if (currRetries < maxRetries) {
LOG.debug("Exception encountered while connecting to "
+ "the server : " + ex);
// try re-login
if (UserGroupInformation.isLoginKeytabBased()) {
UserGroupInformation.getLoginUser().reloginFromKeytab();
} else {
UserGroupInformation.getLoginUser().reloginFromTicketCache();
}
// have granularity of milliseconds
//we are sleeping with the Connection lock held but since this
//connection instance is being used for connecting to the server
//in question, it is okay
Thread.sleep((rand.nextInt(MAX_BACKOFF) + 1));
return null;
} else {
String msg = "Couldn't setup connection for "
+ UserGroupInformation.getLoginUser().getUserName() + " to "
+ serverPrincipal;
LOG.warn(msg);
throw (IOException) new IOException(msg).initCause(ex);
}
} else {
LOG.warn("Exception encountered while connecting to "
+ "the server : " + ex);
}
if (ex instanceof RemoteException)
throw (RemoteException) ex;
throw new IOException(ex);
}
});
}
/** Connect to the server and set up the I/O streams. It then sends /** Connect to the server and set up the I/O streams. It then sends
* a header to the server and starts * a header to the server and starts
* the connection thread that waits for responses. * the connection thread that waits for responses.
@ -421,81 +478,95 @@ private synchronized boolean setupSaslConnection(final InputStream in2,
private synchronized void setupIOstreams() throws InterruptedException { private synchronized void setupIOstreams() throws InterruptedException {
if (socket != null || shouldCloseConnection.get()) { if (socket != null || shouldCloseConnection.get()) {
return; return;
} }
short ioFailures = 0;
short timeoutFailures = 0;
try { try {
if (LOG.isDebugEnabled()) { if (LOG.isDebugEnabled()) {
LOG.debug("Connecting to "+server); LOG.debug("Connecting to "+server);
} }
short numRetries = 0;
final short MAX_RETRIES = 5;
Random rand = null;
while (true) { while (true) {
try { setupConnection();
this.socket = socketFactory.createSocket(); InputStream inStream = NetUtils.getInputStream(socket);
this.socket.setTcpNoDelay(tcpNoDelay); OutputStream outStream = NetUtils.getOutputStream(socket);
// connection time out is 20s writeRpcHeader(outStream);
NetUtils.connect(this.socket, remoteId.getAddress(), 20000); if (useSasl) {
this.socket.setSoTimeout(pingInterval); final InputStream in2 = inStream;
break; final OutputStream out2 = outStream;
} catch (SocketTimeoutException toe) { UserGroupInformation ticket = remoteId.getTicket();
/* The max number of retries is 45, if (authMethod == AuthMethod.KERBEROS) {
* which amounts to 20s*45 = 15 minutes retries. if (ticket.getRealUser() != null) {
*/ ticket = ticket.getRealUser();
handleConnectionFailure(timeoutFailures++, 45, toe); }
} catch (IOException ie) { }
handleConnectionFailure(ioFailures++, maxRetries, ie); boolean continueSasl = false;
} try {
} continueSasl = ticket
InputStream inStream = NetUtils.getInputStream(socket); .doAs(new PrivilegedExceptionAction<Boolean>() {
OutputStream outStream = NetUtils.getOutputStream(socket); @Override
writeRpcHeader(outStream); public Boolean run() throws IOException {
if (useSasl) { return setupSaslConnection(in2, out2);
final InputStream in2 = inStream; }
final OutputStream out2 = outStream; });
UserGroupInformation ticket = remoteId.getTicket(); } catch (Exception ex) {
if (authMethod == AuthMethod.KERBEROS) { if (rand == null) {
if (ticket.getRealUser() != null) { rand = new Random();
ticket = ticket.getRealUser(); }
handleSaslConnectionFailure(numRetries++, MAX_RETRIES, ex, rand,
ticket);
continue;
}
if (continueSasl) {
// Sasl connect is successful. Let's set up Sasl i/o streams.
inStream = saslRpcClient.getInputStream(inStream);
outStream = saslRpcClient.getOutputStream(outStream);
} else {
// fall back to simple auth because server told us so.
authMethod = AuthMethod.SIMPLE;
header = new ConnectionHeader(header.getProtocol(), header
.getUgi(), authMethod);
useSasl = false;
} }
} }
if (ticket.doAs(new PrivilegedExceptionAction<Boolean>() {
@Override if (doPing) {
public Boolean run() throws IOException { this.in = new DataInputStream(new BufferedInputStream(
return setupSaslConnection(in2, out2); new PingInputStream(inStream)));
}
})) {
// Sasl connect is successful. Let's set up Sasl i/o streams.
inStream = saslRpcClient.getInputStream(inStream);
outStream = saslRpcClient.getOutputStream(outStream);
} else { } else {
// fall back to simple auth because server told us so. this.in = new DataInputStream(new BufferedInputStream(inStream));
authMethod = AuthMethod.SIMPLE;
header = new ConnectionHeader(header.getProtocol(),
header.getUgi(), authMethod);
useSasl = false;
} }
} this.out = new DataOutputStream(new BufferedOutputStream(outStream));
if (doPing) { writeHeader();
this.in = new DataInputStream(new BufferedInputStream
(new PingInputStream(inStream)));
} else {
this.in = new DataInputStream(new BufferedInputStream
(inStream));
}
this.out = new DataOutputStream
(new BufferedOutputStream(outStream));
writeHeader();
// update last activity time // update last activity time
touch(); touch();
// start the receiver thread after the socket connection has been set up // start the receiver thread after the socket connection has been set
start(); // up
start();
return;
}
} catch (IOException e) { } catch (IOException e) {
markClosed(e); markClosed(e);
close(); close();
} }
} }
private void closeConnection() {
if (socket == null) {
return;
}
// close the current connection
try {
socket.close();
} catch (IOException e) {
LOG.warn("Not able to close a socket", e);
}
// set socket to null so that the next call to setupIOstreams
// can start the process of connect all over again.
socket = null;
}
/* Handle connection failures /* Handle connection failures
* *
@ -513,17 +584,8 @@ public Boolean run() throws IOException {
*/ */
private void handleConnectionFailure( private void handleConnectionFailure(
int curRetries, int maxRetries, IOException ioe) throws IOException { int curRetries, int maxRetries, IOException ioe) throws IOException {
// close the current connection
if (socket != null) { closeConnection();
try {
socket.close();
} catch (IOException e) {
LOG.warn("Not able to close a socket", e);
}
}
// set socket to null so that the next call to setupIOstreams
// can start the process of connect all over again.
socket = null;
// throw the exception if the maximum number of retries is reached // throw the exception if the maximum number of retries is reached
if (curRetries >= maxRetries) { if (curRetries >= maxRetries) {

View File

@ -232,6 +232,7 @@ public static boolean isSecurityEnabled() {
// All non-static fields must be read-only caches that come from the subject. // All non-static fields must be read-only caches that come from the subject.
private final User user; private final User user;
private final boolean isKeytab; private final boolean isKeytab;
private final boolean isKrbTkt;
private static final String OS_LOGIN_MODULE_NAME; private static final String OS_LOGIN_MODULE_NAME;
private static final Class<? extends Principal> OS_PRINCIPAL_CLASS; private static final Class<? extends Principal> OS_PRINCIPAL_CLASS;
@ -373,6 +374,15 @@ private void setLogin(LoginContext login) {
this.subject = subject; this.subject = subject;
this.user = subject.getPrincipals(User.class).iterator().next(); this.user = subject.getPrincipals(User.class).iterator().next();
this.isKeytab = !subject.getPrivateCredentials(KerberosKey.class).isEmpty(); this.isKeytab = !subject.getPrivateCredentials(KerberosKey.class).isEmpty();
this.isKrbTkt = !subject.getPrivateCredentials(KerberosTicket.class).isEmpty();
}
/**
* checks if logged in using kerberos
* @return true if the subject logged via keytab or has a Kerberos TGT
*/
public boolean hasKerberosCredentials() {
return isKeytab || isKrbTkt;
} }
/** /**
@ -602,7 +612,7 @@ public synchronized void reloginFromTicketCache()
throws IOException { throws IOException {
if (!isSecurityEnabled() || if (!isSecurityEnabled() ||
user.getAuthenticationMethod() != AuthenticationMethod.KERBEROS || user.getAuthenticationMethod() != AuthenticationMethod.KERBEROS ||
isKeytab) !isKrbTkt)
return; return;
LoginContext login = getLogin(); LoginContext login = getLogin();
if (login == null) { if (login == null) {
@ -726,10 +736,9 @@ public static enum AuthenticationMethod {
/** /**
* Create a proxy user using username of the effective user and the ugi of the * Create a proxy user using username of the effective user and the ugi of the
* real user. * real user.
* * @param user
* @param effective * @param realUser
* user, UGI for real user. * @return proxyUser ugi
* @return
*/ */
public static UserGroupInformation createProxyUser(String user, public static UserGroupInformation createProxyUser(String user,
UserGroupInformation realUser) { UserGroupInformation realUser) {