それもばりばり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 件のコメント:
コメントを投稿