1 /* 2 * Copyright 2019 The Netty Project 3 * 4 * The Netty Project licenses this file to you under the Apache License, version 2.0 (the 5 * "License"); you may not use this file except in compliance with the License. You may obtain a 6 * copy of the License at: 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software distributed under the License 11 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 12 * or implied. See the License for the specific language governing permissions and limitations under 13 * the License. 14 */ 15 package io.grpc.netty; 16 17 import io.netty.channel.ChannelFuture; 18 import io.netty.channel.ChannelFutureListener; 19 import io.netty.channel.ChannelHandlerContext; 20 import io.netty.channel.ChannelPromise; 21 import io.netty.handler.codec.http2.DecoratingHttp2ConnectionEncoder; 22 import io.netty.handler.codec.http2.Http2ConnectionEncoder; 23 import io.netty.handler.codec.http2.Http2Error; 24 import io.netty.handler.codec.http2.Http2Exception; 25 import io.netty.handler.codec.http2.Http2LifecycleManager; 26 import io.netty.util.internal.ObjectUtil; 27 import io.netty.util.internal.logging.InternalLogger; 28 import io.netty.util.internal.logging.InternalLoggerFactory; 29 30 /** 31 * {@link DecoratingHttp2ConnectionEncoder} which guards against a remote peer that will trigger a massive amount 32 * of control frames but will not consume our responses to these. 33 * This encoder will tear-down the connection once we reached the configured limit to reduce the risk of DDOS. 34 */ 35 final class Http2ControlFrameLimitEncoder extends DecoratingHttp2ConnectionEncoder { 36 private static final InternalLogger logger = InternalLoggerFactory.getInstance(Http2ControlFrameLimitEncoder.class); 37 38 private final int maxOutstandingControlFrames; 39 private final ChannelFutureListener outstandingControlFramesListener = new ChannelFutureListener() { 40 @Override 41 public void operationComplete(ChannelFuture future) { 42 outstandingControlFrames--; 43 } 44 }; 45 private Http2LifecycleManager lifecycleManager; 46 private int outstandingControlFrames; 47 private boolean limitReached; 48 Http2ControlFrameLimitEncoder(Http2ConnectionEncoder delegate, int maxOutstandingControlFrames)49 Http2ControlFrameLimitEncoder(Http2ConnectionEncoder delegate, int maxOutstandingControlFrames) { 50 super(delegate); 51 this.maxOutstandingControlFrames = ObjectUtil.checkPositive(maxOutstandingControlFrames, 52 "maxOutstandingControlFrames"); 53 } 54 55 @Override lifecycleManager(Http2LifecycleManager lifecycleManager)56 public void lifecycleManager(Http2LifecycleManager lifecycleManager) { 57 this.lifecycleManager = lifecycleManager; 58 super.lifecycleManager(lifecycleManager); 59 } 60 61 @Override writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise)62 public ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise) { 63 ChannelPromise newPromise = handleOutstandingControlFrames(ctx, promise); 64 if (newPromise == null) { 65 return promise; 66 } 67 return super.writeSettingsAck(ctx, newPromise); 68 } 69 70 @Override writePing(ChannelHandlerContext ctx, boolean ack, long data, ChannelPromise promise)71 public ChannelFuture writePing(ChannelHandlerContext ctx, boolean ack, long data, ChannelPromise promise) { 72 // Only apply the limit to ping acks. 73 if (ack) { 74 ChannelPromise newPromise = handleOutstandingControlFrames(ctx, promise); 75 if (newPromise == null) { 76 return promise; 77 } 78 return super.writePing(ctx, ack, data, newPromise); 79 } 80 return super.writePing(ctx, ack, data, promise); 81 } 82 83 @Override writeRstStream( ChannelHandlerContext ctx, int streamId, long errorCode, ChannelPromise promise)84 public ChannelFuture writeRstStream( 85 ChannelHandlerContext ctx, int streamId, long errorCode, ChannelPromise promise) { 86 ChannelPromise newPromise = handleOutstandingControlFrames(ctx, promise); 87 if (newPromise == null) { 88 return promise; 89 } 90 return super.writeRstStream(ctx, streamId, errorCode, newPromise); 91 } 92 handleOutstandingControlFrames(ChannelHandlerContext ctx, ChannelPromise promise)93 private ChannelPromise handleOutstandingControlFrames(ChannelHandlerContext ctx, ChannelPromise promise) { 94 if (!limitReached) { 95 if (outstandingControlFrames == maxOutstandingControlFrames) { 96 // Let's try to flush once as we may be able to flush some of the control frames. 97 ctx.flush(); 98 } 99 if (outstandingControlFrames == maxOutstandingControlFrames) { 100 limitReached = true; 101 Http2Exception exception = Http2Exception.connectionError(Http2Error.ENHANCE_YOUR_CALM, 102 "Maximum number %d of outstanding control frames reached", maxOutstandingControlFrames); 103 logger.info("Maximum number {} of outstanding control frames reached. Closing channel {}", 104 maxOutstandingControlFrames, ctx.channel(), exception); 105 106 // First notify the Http2LifecycleManager and then close the connection. 107 lifecycleManager.onError(ctx, true, exception); 108 ctx.close(); 109 } 110 outstandingControlFrames++; 111 112 // We did not reach the limit yet, add the listener to decrement the number of outstanding control frames 113 // once the promise was completed 114 return promise.unvoid().addListener(outstandingControlFramesListener); 115 } 116 return promise; 117 } 118 } 119