View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.hadoop.hbase.ipc;
19  
20  import io.netty.util.HashedWheelTimer;
21  import io.netty.util.Timeout;
22  import io.netty.util.TimerTask;
23  
24  import java.io.IOException;
25  import java.lang.reflect.InvocationTargetException;
26  import java.lang.reflect.Method;
27  import java.net.InetAddress;
28  import java.net.InetSocketAddress;
29  import java.net.UnknownHostException;
30  import java.util.concurrent.TimeUnit;
31  
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  import org.apache.hadoop.conf.Configuration;
35  import org.apache.hadoop.hbase.HConstants;
36  import org.apache.hadoop.hbase.classification.InterfaceAudience;
37  import org.apache.hadoop.hbase.client.MetricsConnection;
38  import org.apache.hadoop.hbase.codec.Codec;
39  import org.apache.hadoop.hbase.protobuf.ProtobufUtil;
40  import org.apache.hadoop.hbase.protobuf.generated.AuthenticationProtos;
41  import org.apache.hadoop.hbase.protobuf.generated.RPCProtos.ConnectionHeader;
42  import org.apache.hadoop.hbase.protobuf.generated.RPCProtos.UserInformation;
43  import org.apache.hadoop.hbase.security.AuthMethod;
44  import org.apache.hadoop.hbase.security.SecurityInfo;
45  import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
46  import org.apache.hadoop.io.Text;
47  import org.apache.hadoop.io.compress.CompressionCodec;
48  import org.apache.hadoop.security.SecurityUtil;
49  import org.apache.hadoop.security.UserGroupInformation;
50  import org.apache.hadoop.security.token.Token;
51  import org.apache.hadoop.security.token.TokenIdentifier;
52  import org.apache.hadoop.security.token.TokenSelector;
53  import org.apache.hadoop.util.Time;
54  
55  /**
56   * Base class for ipc connection.
57   */
58  @InterfaceAudience.Private
59  abstract class RpcConnection {
60  
61    private static final Log LOG = LogFactory.getLog(RpcConnection.class);
62  
63    protected final ConnectionId remoteId;
64  
65    protected final AuthMethod authMethod;
66  
67    protected final boolean useSasl;
68  
69    protected final Token<? extends TokenIdentifier> token;
70  
71    protected final String serverPrincipal; // server's krb5 principal name
72  
73    protected final int reloginMaxBackoff; // max pause before relogin on sasl failure
74  
75    protected final Codec codec;
76  
77    protected final CompressionCodec compressor;
78  
79    protected final MetricsConnection metrics;
80  
81    protected final HashedWheelTimer timeoutTimer;
82  
83    // the last time we were picked up from connection pool.
84    protected long lastTouched;
85  
86    // Determines whether we need to do an explicit clearing of kerberos tickets with relogin
87    private boolean forceReloginEnabled;
88    // Minimum time between two force re-login attempts
89    private int minTimeBeforeForceRelogin;
90  
91    // Time when last forceful re-login was attempted
92    private long lastForceReloginAttempt = -1;
93  
94    protected RpcConnection(Configuration conf, HashedWheelTimer timeoutTimer, ConnectionId remoteId,
95        String clusterId, boolean isSecurityEnabled, Codec codec, CompressionCodec compressor,
96        MetricsConnection metrics) throws IOException {
97      this.timeoutTimer = timeoutTimer;
98      this.codec = codec;
99      this.compressor = compressor;
100     this.metrics = metrics;
101     UserGroupInformation ticket = remoteId.getTicket().getUGI();
102     SecurityInfo securityInfo = SecurityInfo.getInfo(remoteId.getServiceName());
103     this.useSasl = isSecurityEnabled;
104     Token<? extends TokenIdentifier> token = null;
105     String serverPrincipal = null;
106     if (useSasl && securityInfo != null) {
107       AuthenticationProtos.TokenIdentifier.Kind tokenKind = securityInfo.getTokenKind();
108       if (tokenKind != null) {
109         TokenSelector<? extends TokenIdentifier> tokenSelector = AbstractRpcClient.TOKEN_HANDLERS
110             .get(tokenKind);
111         if (tokenSelector != null) {
112           token = tokenSelector.selectToken(new Text(clusterId), ticket.getTokens());
113         } else if (LOG.isDebugEnabled()) {
114           LOG.debug("No token selector found for type " + tokenKind);
115         }
116       }
117       String serverKey = securityInfo.getServerPrincipal();
118       if (serverKey == null) {
119         throw new IOException("Can't obtain server Kerberos config key from SecurityInfo");
120       }
121       if (metrics != null) {
122         metrics.incrNsLookups();
123       }
124       InetSocketAddress remoteAddr = remoteId.getAddress().toSocketAddress();
125       if (remoteAddr.isUnresolved()) {
126         if (metrics != null) {
127           metrics.incrNsLookupsFailed();
128         }
129         throw new UnknownHostException(remoteId.getAddress() + " could not be resolved");
130       }
131       serverPrincipal = SecurityUtil.getServerPrincipal(conf.get(serverKey),
132         getHostnameForServerPrincipal(conf, remoteAddr.getAddress()));
133       if (LOG.isDebugEnabled()) {
134         LOG.debug("RPC Server Kerberos principal name for service=" + remoteId.getServiceName()
135             + " is " + serverPrincipal);
136       }
137     }
138     this.token = token;
139     this.serverPrincipal = serverPrincipal;
140     if (!useSasl) {
141       authMethod = AuthMethod.SIMPLE;
142     } else if (token != null) {
143       authMethod = AuthMethod.DIGEST;
144     } else {
145       authMethod = AuthMethod.KERBEROS;
146     }
147 
148     if (LOG.isDebugEnabled()) {
149       LOG.debug("Use " + authMethod + " authentication for service " + remoteId.serviceName
150           + ", sasl=" + useSasl);
151     }
152 
153     reloginMaxBackoff = conf.getInt(HConstants.HBASE_RELOGIN_MAXBACKOFF, 5000);
154     this.remoteId = remoteId;
155 
156     forceReloginEnabled = conf.getBoolean(HConstants.HBASE_FORCE_RELOGIN_ENABLED, true);
157     // Default minimum time between force relogin attempts is 10 minutes
158     this.minTimeBeforeForceRelogin =
159         conf.getInt(HConstants.HBASE_MINTIME_BEFORE_FORCE_RELOGIN, 10 * 60 * 1000);
160   }
161 
162   private static boolean useCanonicalHostname(Configuration conf) {
163     return !conf.getBoolean(
164       HConstants.UNSAFE_HBASE_CLIENT_KERBEROS_HOSTNAME_DISABLE_REVERSEDNS,
165       HConstants.DEFAULT_UNSAFE_HBASE_CLIENT_KERBEROS_HOSTNAME_DISABLE_REVERSEDNS);
166   }
167 
168   public static String getHostnameForServerPrincipal(Configuration conf, InetAddress addr) {
169     final String hostname;
170 
171     if (useCanonicalHostname(conf)) {
172       hostname = addr.getCanonicalHostName();
173       if (hostname.equals(addr.getHostAddress())) {
174         LOG.warn("Canonical hostname for SASL principal is the same with IP address: "
175           + hostname + ", " + addr.getHostName() + ". Check DNS configuration or consider "
176           + HConstants.UNSAFE_HBASE_CLIENT_KERBEROS_HOSTNAME_DISABLE_REVERSEDNS
177           + "=true");
178       }
179     } else {
180       hostname = addr.getHostName();
181     }
182 
183     return hostname.toLowerCase();
184   }
185 
186   private UserInformation getUserInfo(UserGroupInformation ugi) {
187     if (ugi == null || authMethod == AuthMethod.DIGEST) {
188       // Don't send user for token auth
189       return null;
190     }
191     UserInformation.Builder userInfoPB = UserInformation.newBuilder();
192     if (authMethod == AuthMethod.KERBEROS) {
193       // Send effective user for Kerberos auth
194       userInfoPB.setEffectiveUser(ugi.getUserName());
195     } else if (authMethod == AuthMethod.SIMPLE) {
196       // Send both effective user and real user for simple auth
197       userInfoPB.setEffectiveUser(ugi.getUserName());
198       if (ugi.getRealUser() != null) {
199         userInfoPB.setRealUser(ugi.getRealUser().getUserName());
200       }
201     }
202     return userInfoPB.build();
203   }
204 
205   protected UserGroupInformation getUGI() {
206     UserGroupInformation ticket = remoteId.getTicket().getUGI();
207     if (authMethod == AuthMethod.KERBEROS) {
208       if (ticket != null && ticket.getRealUser() != null) {
209         ticket = ticket.getRealUser();
210       }
211     }
212     return ticket;
213   }
214 
215   protected boolean shouldAuthenticateOverKrb() throws IOException {
216     UserGroupInformation loginUser = UserGroupInformation.getLoginUser();
217     UserGroupInformation currentUser = UserGroupInformation.getCurrentUser();
218     UserGroupInformation realUser = currentUser.getRealUser();
219     return authMethod == AuthMethod.KERBEROS && loginUser != null &&
220     // Make sure user logged in using Kerberos either keytab or TGT
221         loginUser.hasKerberosCredentials() &&
222         // relogin only in case it is the login user (e.g. JT)
223         // or superuser (like oozie).
224         (loginUser.equals(currentUser) || loginUser.equals(realUser));
225   }
226 
227   protected void relogin() throws IOException {
228     if (UserGroupInformation.isLoginKeytabBased()) {
229       if (shouldForceRelogin()) {
230         LOG.debug(
231           "SASL Authentication failure. Attempting a forceful re-login for "
232               + UserGroupInformation.getLoginUser().getUserName());
233         Method logoutUserFromKeytab;
234         Method forceReloginFromKeytab;
235         try {
236           logoutUserFromKeytab = UserGroupInformation.class.getMethod("logoutUserFromKeytab");
237           forceReloginFromKeytab = UserGroupInformation.class.getMethod("forceReloginFromKeytab");
238         } catch (NoSuchMethodException e) {
239           // This shouldn't happen as we already check for the existence of these methods before
240           // entering this block
241           throw new RuntimeException("Cannot find forceReloginFromKeytab method in UGI");
242         }
243         logoutUserFromKeytab.setAccessible(true);
244         forceReloginFromKeytab.setAccessible(true);
245         try {
246           logoutUserFromKeytab.invoke(UserGroupInformation.getLoginUser());
247           forceReloginFromKeytab.invoke(UserGroupInformation.getLoginUser());
248         } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
249           throw new RuntimeException(e.getCause());
250         }
251       } else {
252         UserGroupInformation.getLoginUser().reloginFromKeytab();
253       }
254     } else {
255       UserGroupInformation.getLoginUser().reloginFromTicketCache();
256     }
257   }
258 
259   private boolean shouldForceRelogin() {
260     if (!forceReloginEnabled) {
261       return false;
262     }
263     long now = Time.now();
264     // If the last force relogin attempted is less than the configured minimum time, revert to the
265     // default relogin method of UGI
266     if (lastForceReloginAttempt != -1
267         && (now - lastForceReloginAttempt < minTimeBeforeForceRelogin)) {
268       LOG.debug("Not attempting to force re-login since the last attempt is less than "
269           + minTimeBeforeForceRelogin + " millis");
270       return false;
271     }
272     try {
273       // Check if forceRelogin method is available in UGI using reflection
274       UserGroupInformation.class.getMethod("forceReloginFromKeytab");
275       UserGroupInformation.class.getMethod("logoutUserFromKeytab");
276     } catch (NoSuchMethodException e) {
277       LOG.debug(
278         "forceReloginFromKeytab method not available in UGI. Skipping to attempt force relogin");
279       return false;
280     }
281     lastForceReloginAttempt = now;
282     return true;
283   }
284 
285   protected void scheduleTimeoutTask(final Call call) {
286     if (call.timeout > 0) {
287       call.timeoutTask = timeoutTimer.newTimeout(new TimerTask() {
288 
289         @Override
290         public void run(Timeout timeout) throws Exception {
291           call.setTimeout(new CallTimeoutException("Call id=" + call.id + ", waitTime="
292               + (EnvironmentEdgeManager.currentTime() - call.getStartTime()) + ", rpcTimetout="
293               + call.timeout));
294           callTimeout(call);
295         }
296       }, call.timeout, TimeUnit.MILLISECONDS);
297     }
298   }
299 
300   protected byte[] getConnectionHeaderPreamble() {
301     // Assemble the preamble up in a buffer first and then send it. Writing individual elements,
302     // they are getting sent across piecemeal according to wireshark and then server is messing
303     // up the reading on occasion (the passed in stream is not buffered yet).
304 
305     // Preamble is six bytes -- 'HBas' + VERSION + AUTH_CODE
306     int rpcHeaderLen = HConstants.RPC_HEADER.length;
307     byte[] preamble = new byte[rpcHeaderLen + 2];
308     System.arraycopy(HConstants.RPC_HEADER, 0, preamble, 0, rpcHeaderLen);
309     preamble[rpcHeaderLen] = HConstants.RPC_CURRENT_VERSION;
310     synchronized (this) {
311       preamble[rpcHeaderLen + 1] = authMethod.code;
312     }
313     return preamble;
314   }
315 
316   protected ConnectionHeader getConnectionHeader() {
317     ConnectionHeader.Builder builder = ConnectionHeader.newBuilder();
318     builder.setServiceName(remoteId.getServiceName());
319     UserInformation userInfoPB;
320     if ((userInfoPB = getUserInfo(remoteId.ticket.getUGI())) != null) {
321       builder.setUserInfo(userInfoPB);
322     }
323     if (this.codec != null) {
324       builder.setCellBlockCodecClass(this.codec.getClass().getCanonicalName());
325     }
326     if (this.compressor != null) {
327       builder.setCellBlockCompressorClass(this.compressor.getClass().getCanonicalName());
328     }
329     builder.setVersionInfo(ProtobufUtil.getVersionInfo());
330     return builder.build();
331   }
332 
333   protected abstract void callTimeout(Call call);
334 
335   public ConnectionId remoteId() {
336     return remoteId;
337   }
338 
339   public long getLastTouched() {
340     return lastTouched;
341   }
342 
343   public void setLastTouched(long lastTouched) {
344     this.lastTouched = lastTouched;
345   }
346 
347   /**
348    * Tell the idle connection sweeper whether we could be swept.
349    */
350   public abstract boolean isActive();
351 
352   /**
353    * Just close connection. Do not need to remove from connection pool.
354    */
355   public abstract void shutdown();
356 
357   public abstract void sendRequest(Call call, HBaseRpcController hrc) throws IOException;
358 
359   /**
360    * Does the clean up work after the connection is removed from the connection pool
361    */
362   public abstract void cleanupConnection();
363 }