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:
parent
1035138b4c
commit
c47d34a866
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user