Translate

2013年2月15日金曜日

Google App EngineのDDoS対策としてServletFilterを作ってみる

私が昔作ったGoogle App Engine for Javaベース、
それもばりばりServletゴリ書きしたサイトが
分散DoS攻撃を受けていることは
まえの記事に書いた。

作りがDDoS攻撃なんか受けることを想定していなかったので
1リクエスト=1DB更新
というとんでもないプログラムになっていて
それを直すには時間がかかりそうなので
てっとり早く何とかしないといけない...


で、考えたのが、とりあえず対処療法。

Servletを呼び出すリクエストすべてをフックする
分散DDoS対策フィルタをかませることにした。

いろんなIPアドレスからたくさんのリクエストを送ってくるので
・IPアドレスごとにアクセス数をカウントする
・所定の時間カウントを保持するが、その後自動的にクリアする
・BigTableは使用しないでmemcacheのみで実装する
ということにして作ってみた。

参考にされる方はat your own riskでお願いします。



まずフィルタクラスAntiDDoSFilter.java

package harahara-dev;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import harahara-dev.FrequencyManager;
/**
 * <p>対DDoS攻撃用ServletFilter</p>
 * <p>リクエスト送信元のIPアドレス別にアクセス回数をmemcacheで管理し、
 * 一定ルールを超えた場合、所定のページヘfowardする。</p>
 * @author harahara-development
 */
public class AntiDDoSFilter implements Filter{
 
 /**
  * ロガー
  */
 private static Logger log =
   Logger.getLogger(AntiDDoSFilter.class.getName());
 
 /**
  * FilterConfigをインスタンス
  */
 private FilterConfig config = null;

 /**
  * 初期処理。FilterConfigインスタンスをインスタンス変数へ格納する。
  * @param FilterConfig FilterConfigインスタンス
  */
 @Override
 public void init(FilterConfig config) throws ServletException {
  this.config = config;
 }

 /**
  * フィルタ処理。
  * memcache上のIPアドレスアクセス数を比較し違反した場合、
  * 所定のページヘフォワーディングする。
  * @param ServletRequest req リクエスト
  * @param ServletResponse resp レスポンス
  * @param FilterChain フィルタチェーン
  */
 @Override
 public void doFilter(ServletRequest req, ServletResponse resp,
   FilterChain chain) throws IOException, ServletException {
  String ip = req.getRemoteAddr();
  FrequencyManager freqManager = FrequencyManager.getInstance();
  boolean result = freqManager.isHeavyAccess(ip);
  if(result){
   log.log(Level.INFO, "[doFilter] ip=" + ip +" is heavy user");
   ServletContext context = config.getServletContext();
   RequestDispatcher rd = context.getRequestDispatcher("/toomanyaccess.html");
   try {
    rd.forward(req, resp);
    log.info("[doFilter] fowarding done");
   } catch (Exception e) {
    log.log(Level.INFO,
      "[doFilter] exception during forwarding", e);
   }
  }
  // 処理が終わったので、後続のフィルタ/サーブレットへ
  chain.doFilter(req,  resp);
 }

 /**
  * 終了処理。
  * インスタンス変数をクリアする。
  */
 @Override
 public void destroy() {
  config = null;
 }
}

つぎにmemcacheを使った頻度管理マネージャFrequencyManager.java

package harahara-dev;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.appengine.api.memcache.jsr107cache.GCacheFactory;
import net.sf.jsr107cache.Cache;
import net.sf.jsr107cache.CacheException;
import net.sf.jsr107cache.CacheFactory;
import net.sf.jsr107cache.CacheManager;

/**
 * 頻度管理マネージャ
 * @author harahara-development
 */
public class FrequencyManager implements Serializable {

 /**
  * シリアルバージョンUID
  */
 private static final long serialVersionUID = -1111111111111111111111L;
 
 /**
  * キャッシュクリアされるデフォルトのタイムアウト時間(秒)
  */
 public static final int DEF_TIMEOUT = 3600;
 
 /**
  * アクセス回数上限(リクエスト数)
  */
 public static final int DEF_MAXCOUNTS = 200;
 
 /**
  * appengine-web.xml上に定義するプロパティキー:タイムアウト時間(秒)
  */
 public static final String PROPKEY_TIMEOUT = "cache.timeout";
 
 /**
  * appengine-web.xml上に定義するプロパティキー:リクエスト上限回数
  */
 public static final String PROPKEY_MAXCOUNTS = "cache.maxcounts";
 
 /**
  * キャッシュ名
  */
 public static final String CACHE_NAME = "frequnecy";
 
 /**
  * ロガー(App Engine)
  */
 private static final Logger log =
   Logger.getLogger(FrequencyManager.class.getName());
 
 /**
  * 唯一のFrequencyManagerインスタンス
  */
 private static final FrequencyManager freqManager = new FrequencyManager();

 private int maxCounts = 200;
 private int timeout = DEF_TIMEOUT;

 /**
  * 外部から呼び出し不可のコンストラクタ。
  * プロパティ値が設定されていればその値でタイムアウト、上限アクセス数を変更し、
  * キャッシュを生成する。
  * キャッシュ生成時の例外は無視される(App Engineログへ出力)。
  */
 @SuppressWarnings("unchecked")
 private FrequencyManager(){
  // タイムアウト値の確定
  String propTimeout = System.getProperty(PROPKEY_TIMEOUT);
  if(propTimeout!=null){
   try{
    timeout = Integer.parseInt(propTimeout);
   }catch(RuntimeException e){
    log.log(Level.INFO, "Not numeric prop timeout=" +
      propTimeout, e);
   }
  }
  
  // 最大アクセス回数の確定
  String propMaxCounts = System.getProperty(PROPKEY_MAXCOUNTS);
  if(propMaxCounts!=null){
   try{
    maxCounts = Integer.parseInt(propMaxCounts);
   }catch(RuntimeException e){
    log.log(Level.INFO, "Not numeric prop max counts" +
      propMaxCounts, e);
   }
  }
  
  // キャッシュの生成
  try {
   @SuppressWarnings("rawtypes")
   // キャッシュ設定用Map生成
   Map props = new HashMap();
   // タイムアウト値を設定
   props.put(GCacheFactory.EXPIRATION_DELTA, timeout);
   // キャッシュ名を設定
   props.put("name", CACHE_NAME);
   // キャッシュマネージャを取得
   CacheManager manager = CacheManager.getInstance();
   // キャッシュファクトリを取得
   CacheFactory factory = manager.getCacheFactory();
   // キャッシュを生成
   Cache cache = factory.createCache(props);
   // 生成したキャッシュをキャッシュ名をつけて登録
   manager.registerCache(CACHE_NAME, cache);
  } catch (CacheException e) {
   // 例外は再スローしない
   log.log(Level.WARNING, "Exception in constructor", e);
  }
 }
 
 /**
  * FrequencyManagerインスタンスを取得する。
  * @return FrequencyManager インスタンス
  */
 public static final FrequencyManager getInstance(){
  return freqManager;
 }
 
 /**
  * 指定したipアドレスの利用回数が上限に達しているかを確認する。
  * @param ip IPアドレス
  * @return boolean 真:超過している、偽:範囲内
  */
 public boolean isHeavyAccess(String ip){
  log.log(Level.INFO, "isHeavyAccess() arrived ip=[" + ip +"]");
  Frequency freq = null;
  Cache cache = CacheManager.getInstance().getCache(CACHE_NAME);
  freq = (Frequency) cache.get(ip);
  if(freq==null){
   freq = new Frequency(timeout, maxCounts);
   cache.put(ip, freq);
   log.log(Level.INFO, "create Frequency ip=" + ip);
  }
  boolean result = freq.isHeavyAccess();
  log.log(Level.INFO, "isHeavyAccess() returns " + result);
  return result;
 }

 /**
  * 頻度クラス(インナークラス)。
  * キャッシュに格納される唯一のオブジェクト。
  * @author harahara-development
  */
 class Frequency implements Serializable{
  /**
   * シリアルバージョンUID
   */
  private static final long serialVersionUID = -2222222222222222222L;
  /**
   * デフォルトの最大アクセス回数
   */
  private int maxCounts = DEF_MAXCOUNTS;
  /**
   * デフォルトのタイムアウト値(ミリ秒)
   */
  private long timeoutMills = DEF_TIMEOUT*1000L;
  /**
   * インスタンス生成時刻
   */
  private long createdAtMills = System.currentTimeMillis();
  /**
   * アクセス回数
   */
  private int count = 0;
  /**
   * インスタンスを生成する。
   * @param timeout タイムアウト時間(秒)
   * @param maxCounts 最大アクセス回数
   */
  Frequency(int timeout, int maxCounts){
   this.timeoutMills = timeout*1000L;
   this.maxCounts = maxCounts;
  }
  /**
   * 利用回数が上限に達しているかを確認する。
   * 判断とともにカウンタを1回加算する。
   * @return boolean 真:超過している、偽:範囲内
   */
  public boolean isHeavyAccess(){
   // 有効期限を超えた場合、カウンタをリセットする
   if(System.currentTimeMillis()>(createdAtMills+timeoutMills)){
    count = 0;
    createdAtMills = System.currentTimeMillis();
    log.log(Level.INFO, "Frequency reseted");
    return true;
   }
   // カウンタを1加算し最大カウント回数を超過したかどうかbooleanを返却する
   return ((++count)>maxCounts);
  }
  
  @Override
  public String toString(){
   return "Count(" + count + "/" + maxCounts + ")";
  }
 }
}


最後にフィルタの設定をWEB-INF/web.xmlのなかへ記述する。

 ...
 <filter>
  <filter-name>antiDDoS</filter-name>
  <filter-class>harahara-dev.AntiDDoSFilter</filter-class>
  <init-param>
   <!-- cache values timeout (sec) -->
   <param-name>cache.timeout</param-name>
   <param-value>3600</param-value>
  </init-param>
  <init-param>
   <!-- cache values max request counts -->
   <param-name>cache.maxcounts</param-name>
   <param-value>200</param-value>
  </init-param>
 </filter>
 
 <filter-mapping>
  <filter-name>antiDDoS</filter-name>
  <url-pattern>/*</url-pattern>
 </filter-mapping>
 ...

一応言っておくとこのurl-patternでは管理系サーブレットも
頻度管理されるので、気をつけること。




..で、これをセットしてしばらくたつが、
結局BigTableアクセスの頻度は多少おとすことができた。


ただ、
もっとDDoS送信元偽装が浅く広くされてしまうと
当然こんな対処方法ではすぐ手に負えなくなる。


1リクエスト→1DB更新
ではなく、
遅延書き込みというかまとめ書きをしないと
対処できん..

いよいよ全書き換えかなあ..

0 件のコメント:

既存アプリケーションをK8s上でコンテナ化して動かす場合の設計注意事項メモ

既存アプリをK8sなどのコンテナにして動かすには、どこを注意すればいいか..ちょっと調べたときの注意事項をメモにした。   1. The Twelve Factors (日本語訳からの転記) コードベース   バージョン管理されている1つのコードベースと複数のデプロイ 依存関係 ...