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 件のコメント:

ClaudeをOpenAI O1のように思考させるDifyサンプルを試す

 Difyの「探索」タブにはさまざまなサンプルが載っており、その1つに「Thinking Claude (OpenAI O1 Alternative)」というものがある。   このサンプルがどういうものか、未だに自分でも解けない以下の問題をためしに聞いてみることにした。 『人類の...