Edited at

IIJmioでジェイムズくん流ケチケチ運用

私はIIJmioを3回線、合計一ヶ月13GBの枠で使っております…が、時々、月末にクーポン不足(いわゆる「ギガ不足」)になります。

そこで、QoSを維持しつつ、ジェイムズくんもビックリのケチケチ運用をすることにしました。


  • トラヒックが100kbpsを上回ったら、クーポンをオンにする

  • トラヒックが200kbpsを下回ったら、クーポンをオフにする

  • 「クーポン非使用時は3日で366MB以上通信をすると規制」にひっかからないようにする

アプリのバックグラウンド通信などはクーポンを使わさせず、人間が操作しているときだけクーポンを使おう、という戦略です。

このためには、以下の問題を解決する必要があります。


  • IIJmio APIの使用量はメガバイト単位で帰ってくるので、byte per secの粒度で速度を求めることはできない

  • iOSのデバイスの通信量をiOSのアプリから調べるのは難しい

今回は通信量を調べていますが、スマホの画面が点灯しているのか消灯しているのかを調べても良いかもしれませんね、方法はちょっと思いつきませんが。


iOSの通信量を観測

オンデマンドVPNを常に張るよううにして、VPNのもう片方の端点で計測します。

IIJmio(というかDoCoMo?)が計測しているトラヒックとは多少違うかもしれませんが、多少の再送は誤差でしょうからヨシとします。


OpenVPNをいれる (サーバサイド)

IPを常時話せるマシンにOpenVPNをいれて待機させます。

今回はiptablesを使う関係で、linuxでいきます。

udp port 1234に待機させるとします。

dev tun0

lport 1234
ping 60
ping-restart 120
float
comp-lzo adaptive
push "comp-lzo adaptive”

client-connect "/usr/local/bin/connected-iphone.pl"
script-security 2

push "redirect-gateway def1"
push "dhcp-option DNS 192.168.100.1"

以下お好みで…

iOSのOpenVPNクライアントは使える暗号アルゴリズムに制約があるので、注意しましょう。


client-connectスクリプトを書く

接続してきたクラアイントのIPアドレスから、それがWiFiなのかIIJmioの回線なのか判別してどこかに記録するようにします。

環境変数でいろいろ貰えます。

プログラム省略。


iptablesで通信量を測定する

openvpnのstateファイルで見てもいいのですが、iptablesで調べれば十分な粒度がでます。


ルール作成

iptablesに以下のような計測のためだけのルールをいれます。

iptables -A INPUT -p udp -m udp -s IIJMIONET --dport 1234 -j count-ovpn-iphone-cell-dev

iptables -A OUTPUT -p udp -m udp -d IIJMIONET --sport 1234 -j count-ovpn-iphone-cell-dev
iptables -A count-ovpn-iphone-cell-dev -j RETURN

IIJMIONETの部分には、IIJmioで使うIPv4アドレスブロックを列挙します。

これは公表されていませんが、上記のclinet-connectで集めたりdigったりした限り、以下で大丈夫っポイです。

49.239.64.0/18

163.49.128.0/17
202.214.0.0/16
202.221.0.0/16
202.232.0.0/16
202.32.0.0/16
203.180.0.0/16
210.128.0.0/16
210.130.0.0/16
210.138.0.0/16
210.148.0.0/16
210.149.0.0/16


集計

/sbin/iptables -nxvLで上記のルールを通過したパケットの量が出ます。

プログラム省略。

トンネルデバイスのtunも一緒に集計すると、OpenVPNの圧縮でどのくらいトクをしたか、あるいはヘッダの追加分でどのくらい損をしたかが分かるようになります。

httpsやすでに圧縮されたデータ転送が多いご時世、そんなに得はしませんね。

集計はmunin pluginの形にしておき、muninでグラフを書くとともに後述の制御プログラムからも参照します。


OpenVPNをいれる (iOSクライアントサイド)


OpenVPN Client for iOSをいれる

https://itunes.apple.com/jp/app/openvpn-connect/id590379981?mt=8

をいれます。


オンデマンドVPNを有効にする

mobileconfigのxmlをつくって、iOSに食べさせます。

http://www.codingmerc.com/blog/ios-vpn-on-demand-profile-with-openvpn/

iOSのバージョンアップとOpenVPNのバージョンアップで時々変わるので、気をつけましょう。

OpenVPNのサポートをみていると「Appleがまた仕様変更した」「なんとか対応しました」とかあります(苦笑)。

オンデマンドルールでは、自宅WiFi以外のときは常に繋がるようにします。

これでその辺にある暗号化なしWiFiに繋げても安心ですし、VPN経由で自宅NASの中の映像をみたりイロイロ遊べるようになります。


ダミーのVPNプロファイルをいれる

上記の常時接続VPNだけだと、VPNがうまく使えないとき(WiFiでudpのVPNが規制されているとき1や、VPN設置端点のIPアドレスからのアクセスを規制しているサイト2に接続したいとき、VPNを使っていると文句をいうNetflixを見るとき)に手がでなくなることがあります。

手動でVPNプロファイルを無効化できるように、ダミーのプロファイルを一つ追加しておきます。


経路を調整する

このままでは、このVPN端点そのものに接続するOpenVPN以外のトラヒックはVPNを通らないので、このVPN端点にはプライベートIPv4アドレスを付与して、iOSに対してVPNの中からのdnsクエリーにプライベートIPv4を応答するようなdnsサーバをたて、なるべく多くのトラヒックがOpenVPNを通ってくれるようにします。

そうしないと、自動速度調節で自分のサーバとの通信がカウントされなくなってしまいます。

dnsmasqをたてて、

interface=tun0

address=/dav.example.com/192.168.100.1

といった設定でOpenVPNからの問い合わせに少し偽装した返答を返します。


IIJmioを制御する


IIJmio APIトークンを取得する

IIJmioのAPIコンソールにいって、デベロッパー登録をして、自分で認証をするとaccess_tokenを貰えるようにします。

access_tokenは#で繋がったURLに出るので、javascriptの支援を得てサーバ側で回収します。

プログラム省略。

一度取得したトークンは三ヶ月使えます。

三ヶ月に一回人間がログインするのもバカらしいので、seleniumで更新するようにcronを仕込みます。

この場合、seleniumのdriver.current_urlにトークンが入るのでCGIなどでの回収は不要です。


IIJmioの状態を常にとれるようにする

iijmio APIの制限を回避しながら、クーポンの有効/無効、当日を含めた過去3日間のクーポンなし通信量を集計するプログラムをかいて、IIJmioに怒られない程度の頻度(数分に一回程度)で廻します。

プログラム省略。


IIJmio制御プログラムを書く

ここからが本題です。

ようやくここまできて、


  • iOS端末の秒単位、バイト単位の粒度でのトラヒック情報

  • iOS端末がWiFiなのかセルラー回線なのかどうかの判定

  • IIJmioが規制しそうかどうかの基準となる直近三日通信量

  • IIJmioのクーポンの状態

がとれます。

気をつけることは


  • 複数のSIMがあるときは個別に制御できるが、一回のAPI呼び出しでまとめて設定しないとAPI利用制限の枠を無駄に消費してしまう

  • クーポンなし直近三日通信量はIIJmioの資料では「数時間毎に更新」になっているので、すこし余裕をもたせる

ですね。

perl風コード。

while(1){

#速度確認
my $munin_new=read_munin();
for my $dev (qw(ipad iphone)){
for my $net (qw(wifi cell)){
$bps->{$dev}->{$net}=(($munin_new->{"${dev}_${net}_dev_down"} - $munin_last->{"${dev}_${net}_dev_down"} +
$munin_new->{"${dev}_${net}_dev_up"} - $munin_last->{"${dev}_${net}_dev_up"} )
/
($munin_new->{time} - $munin_last->{time}));
}
}
$munin_last=$munin_new;

#もし規制値に近いときはクーポンを使う
$done=0;
foreach my $d (qw(ipad iphone)){
if(read_data("iijmio last3days $d") > $iij_coupon_limit){
$forc_fast{$d}=true;
setiij($d, true);
$done++;
}
}
if($done){
setiij();
}

#人間がHomeBridge経由で高速・低速をリクエストしたときは3時間はそれに従う
##略

#位置情報関係
##在宅?
if($lastarea eq "out" and
$area eq "in"){
#お家に帰ってきた瞬間
setiij("all", false);
$lastarea=$area;
goto END;
}
if($lastarea eq "in" and
$area eq "out"){
#出て行った瞬間
setiij("all", true);
$lastarea=$area;
goto END;
}
$lastarea=$area;

#自宅にいるときはクーポンは使わない
$done=0;
for my $dev (@DEVICES){
if(ping($dev)){
$done+=setiij($dev, false);
}
}
if($done){
setiij();
goto END;
}

##どこかでwifiつかっているときはクーポンつかわない
$done=0;
for my $dev (@DEVICES){
if(read_data("openvpn $dev") eq "wifi"){
$done+=setiij($dev, false);
$lastnet->{$dev}="wifi";
}
}
if($done){
setiij();
goto END;
}

#wifiから出てcellularに移動した瞬間はONにする
$done=0;
for my $dev (@DEVICES){
if(read_data("openvpn $dev") eq "cellular" and
$lastnet->{$dev} eq "wifi"){
$done+=setiij($dev, true);
$lastnet->{$dev}="cellular";
}
$lastnet->{$dev}=read_data(“openvpn $dev”);
}
if($done){
setiij();
goto END;
}

#速度による制御
$done=0;
for my $dev (@DEVICES){
if(not $force_fast{$dev}){
if((! getiijcoupon($dev)) and ($bps->{$dev}->{cell} > $bps_up_limit)){
$done+=setiij($dev, true);
}elsif((getiijcoupon($dev)) and ($bps->{$dev}->{cell} < $bps_down_limit)){
$done+=setiij($dev, false);
}
}
}
if($done) setiij();

#おわり
END:
sleep(適当な間隔);
}

function setiij(デバイス, クーポン使用のbool)
{
if(デバイス){ 値を控えて置いて、前回の値と異なれば1を返す }
if(not デバイス or デバイス == "all"){ iijにリクエスト(); }

#高頻度にやると拒否されるので、前回のリクエスト時間を記録しておいて、最低でも60秒はあける。
}


変更通知を受け取るようにする

クーポンが変更されたら、変更した旨と理由をiOSで受け取るようにします。

pushoverが使えます。jsonをpostすると、iOSデバイスに通知がきます。


結果をみる


muninを入れる (サーバサイド)

httpdとあわせてdockerで立ち上げます。


munin-nodeを入れる (クライアントサイド)

munin-nodeで上記のスクリプトの出力を返すようにします。


muninで予測をさせる

munin-nodeのgraph_args_afterに、rrdtoolのPRECITを含むRPNを入れ、一ヶ月周期で予測させます。


結果

いい感じですね。

iijmioはSIMの枚数で契約が決まるため、「SIMの枚数そのまま、容量を減らして減額」という契約変更はできません。余剰をスマホの写真自動アップロードやNetflix, dアニメなどに廻すのがいいですね。

SIMを追加して家族のスマホを入れるという手もあります。


半年運用してみての感想

iPadを取り出してなにかし始めた瞬間、すこし遅いのが気になることはありますが、許容範囲内です。

VPNを使っているとネトフリにリージョン回避を疑われて拒否されるので、音泉、dアニメ、YouTube、観劇三昧、ottavaあたりで消費しておりますが、それでもクーポンはかなり貯まります。

本体ストレージにデータを入れて持ち運ぶ必要もなくなるので、次の端末更新では端末のストレージサイズを減らすことも可能かもしれません。


Future Work


  • 結構いいかげんな制御ですが、もうすこし賢いヒステリシス曲線をいれるとか、時系列解析、機械学習とかさせると、「人間が触りだすと5秒でクーポンON」とかできるかもしれません。

  • iOSがTOS, DiffServ, Traffic ClassなどのQoSのためのIPヘッダ部分を使ってくれるのかどうかは知りませんが、「優先してくれと主張するパケットが多いときはクーポンON」という手もあるかもしれません。

  • QoSをみなくても、httpsの接続先を調べて「twitterなら低速でいいよ」等とする手もあります。自分のトラヒックを自分で監視するだけですから、通信の秘密上は何ら問題はありません。

  • 脱獄すればいくらでも手はあるのでしょうが、それは流石にねぇ...


免責と注意


  • IIJmio API利用規約違反ではないとは思いますが、何か言われても責任はとれません。

  • あまり激しく制御すると、IIJmio約款・規約18条(利用停止)「当社が不適切と判断する態様においてIIJmioサービスを利用したとき」等の条項を適用されてしまう可能性はあります。限界を狙って激しく制御するのはやめましょう。





  1. sslhを使って443/tcpを酷使するという手があります。 



  2. エロスケが一部のVPSを規制しているため。