BluVPN:Weathermap
This article is a part of BluVPN.
Contents |
[edit] Introduction
As you recently may have seen, BluVPN now has a weathermap located at http://stats.thehawken.org/blumap1. We will take a look at how this works.
[edit] Collecting data
In hawken's homedir of the participating servers, there is a spooky script run every minute that grabs interface statistics from /proc/net/dev and connects to 10.159.2.1 on port 31372, where it identifies itself and deposits the current sent/recieved bytes numbers. This should be triggered by cron. Gamma inserts this information into Round Robin databases. The client identifies using an ID number, md5 sum, the correct source address, and the correct number of variables deposited. Gamma checks this against a mysql table, and if some information is incorrect, the client is disconnected. If the client, however, is able to validate and insert its data, it will not be disconnected, and the server waits for another insertion until the client either fails or disconnects by itself. Data is saved into rrd databases, named after the ID. You can access all these databases (There are over 100) at [1].
[edit] Graphing the weathermap
Every minute, the crontab in gamma runs the line
* * * * * services (cd /web/internal/weathermap && ./weathermap --config configs/bluvpn2 --image-uri weathermap.jpg; \
convert -quality 50 /web/stats/blumap1/weathermap{.png,2.jpg}; mv /web/stats/blumap1/weathermap{2,}.jpg) &> /dev/null
Where it chdirs to the weathermap location, runs the script and the script will save a huge PNG file. This huge file gets converted from png to jpg, and is made accessible to you. At the moment, the configuration looks like this:
[edit] Configuration files and other huge scripts
[edit] testclient.php
This file runs every minute and sends statistics to gamma.
<?php
$host = "10.159.2.1";
$port = 31372;
$pass = "";
$fh = fsockopen($host, $port) or die("Could not connect\n");
$data_raw = file_get_contents("/proc/net/dev");
$time = time();
$data_raw = array_slice(explode("\n", trim($data_raw, "\n")), 2);
foreach($data_raw as $l){
$l = explode(":", trim($l, " "), 2);
$if = $l[0];
$split = preg_split('/\s+/', trim($l[1], " "));
$stat = array(
'rx' => array(
'bytes' => $split[0],
'packets' => $split[1],
'errs' => $split[2],
'drop' => $split[3],
'fifo' => $split[4],
'frame' => $split[5],
'compressed' => $split[6],
'multicast' => $split[7]),
'tx' => array(
'bytes' => $split[8],
'packets' => $split[9],
'errs' => $split[10],
'drop' => $split[11],
'fifo' => $split[12],
'colls' => $split[13],
'carrier' => $split[14],
'compressed' => $split[15]),
);
$data = array($stat['rx']['bytes']*8, $stat['tx']['bytes']*8);
if($if=="eth1"){
$id = 8;
insertrecord($fh, $id, $pass, $time, $data);
} else if($if=="eth0"){
$id = 9;
insertrecord($fh, $id, $pass, $time, $data);
} else if($if=="lo"){
$id = 10;
insertrecord($fh, $id, $pass, $time, $data);
} else if($if=="tun6to4"){
$id = 11;
insertrecord($fh, $id, $pass, $time, $data);
} else if($if=="vlan3"){
$id = 12;
insertrecord($fh, $id, $pass, $time, $data);
}
}
fclose($fh);
function insertrecord($fh, $id, $pass, $time, array $data){
fwrite($fh, insert($id, $pass, $time, $data));
get_response($fh, $code, $str);
# echo "($code) $str\n";
}
function insert($id, $password, $time, array $values){
$packet = encode('int', $id).
encode('md5', md5($password)).
encode('int', $time).
encode('short', count($values));
foreach($values as $val) $packet .= encode('short', strlen($val)).$val;
return $packet;
}
function get_response($fh, &$code, &$str){
$data = fread($fh, 4);
if($data === false) die("Socket closed\n");
if(strlen($data) < 4) die("Not enough data from socket\n");
$code = decode('short', substr($data, 0, 2));
$len = decode('short', substr($data, 2, 2));
$str = fread($fh, $len);
if($str === false) die("Socket closed\n");
}
function encode($type, $value){
return code($type, 'encode', $value);
}
function decode($type, $value){
return code($type, 'decode', $value);
}
function code($type, $direction, $value){
if($direction=='decode'){
if($type=='int'){
$tmp = unpack('N', $value);
return $tmp[1];
} else if($type=='short'){
$tmp = unpack('n', $value);
return $tmp[1];
} else if($type=='md5'){
$ret = "";
for($i=0;$i<16;$i++){
$r = dechex(ord($value{$i}));
$ret .= str_repeat("0", 2-strlen($r)).$r;
}
return $ret;
}
} else if($direction=='encode'){
if($type=='int'){
return pack('N', $value);
} else if($type=='short'){
return pack('n', $value);
} else if($type=='md5'){
$ret = "";
for($i=0;$i<16;$i++) $ret .= chr(hexdec(substr($value, $i*2, 2)));
return $ret;
}
}
return false;
}
[edit] Weathermap configuration file
This is the configuration file behind the weathermap.
# Automatically generated by php-weathermap v0.97a
BACKGROUND images/smallworld.png
WIDTH 3599
HEIGHT 1826
HTMLSTYLE overlib
HTMLOUTPUTFILE /web/stats/blumap1/index.html
IMAGEOUTPUTFILE /web/stats/blumap1/weathermap.png
TIMEPOS 80 880 Created: %b %d %Y %H:%M:%S
KEYPOS DEFAULT 90 650 Traffic Load
KEYTEXTCOLOR 0 0 0
KEYOUTLINECOLOR 0 0 0
KEYBGCOLOR 255 255 255
BGCOLOR 255 255 255
TITLECOLOR 0 0 0
TIMECOLOR 0 0 0
SCALE DEFAULT 0 0 192 192 192
SCALE DEFAULT 0 1 255 255 255
SCALE DEFAULT 1 10 140 0 255
SCALE DEFAULT 10 25 32 32 255
SCALE DEFAULT 25 40 0 192 255
SCALE DEFAULT 40 55 0 240 0
SCALE DEFAULT 55 70 240 240 0
SCALE DEFAULT 70 85 255 192 0
SCALE DEFAULT 85 100 255 0 0
SET key_hidezero_DEFAULT 1
# End of global section
# TEMPLATE-only NODEs:
NODE DEFAULT
LABELFONT 2
MAXVALUE 100
# TEMPLATE-only LINKs:
LINK DEFAULT
WIDTH 3
BWFONT 1
BWLABEL bits
BANDWIDTH 100M
# regular NODEs:
NODE gamma
LABEL Gamma
POSITION 1259 92
NODE nyu
LABEL Nyu~
POSITION 1193 212
NODE rei
LABEL Rei
POSITION 367 461
NODE bluserv
LABEL Bluserv
POSITION 1207 89
NODE h121
LABEL H121
POSITION 1144 149
NODE xhr0nohub
LABEL Xhrono VPS
POSITION 113 193
NODE soulnetworks
LABEL soul-networks
POSITION 2172 729
# regular LINKs:
LINK gamma-nyu
OVERLIBGRAPH http://stats.thehawken.org/graph.php?speed=6h&id=79
TARGET scale:1:/root/nms/general-rrd/rrd/79.rrd:stream0:stream1
NODES gamma nyu
VIA 1284 182
BANDWIDTH 10M 1M
LINK nyu-rei
OVERLIBGRAPH http://stats.thehawken.org/graph.php?speed=6h&id=102
TARGET scale:1:/root/nms/general-rrd/rrd/102.rrd:stream0:stream1
NODES nyu rei
BANDWIDTH 30M
LINK gamma-rei
OVERLIBGRAPH http://stats.thehawken.org/graph.php?id=80&speed=6h
TARGET scale:1:/root/nms/general-rrd/rrd/80.rrd:stream0:stream1
NODES gamma rei
VIA 1316 246
BANDWIDTH 10M 1M
LINK bluserv-rei
OVERLIBGRAPH http://stats.thehawken.org/graph.php?id=84&speed=6h
TARGET scale:1:/root/nms/general-rrd/rrd/84.rrd:stream0:stream1
NODES bluserv rei
VIA 795 121
BANDWIDTH 30M
LINK h121-rei
OVERLIBGRAPH http://stats.thehawken.org/graph.php?id=92&speed=6h
TARGET scale:1:/root/nms/general-rrd/rrd/92.rrd:stream0:stream1
NODES h121 rei
VIA 727 268
BANDWIDTH 10M 1M
LINK gamma-bluserv
OVERLIBGRAPH http://stats.thehawken.org/graph.php?id=77&speed=6h
TARGET scale:1:/root/nms/general-rrd/rrd/77.rrd:stream0:stream1
NODES gamma bluserv
VIA 1250 50
VIA 1216 48
BANDWIDTH 10M 1M
LINK bluserv-h121
OVERLIBGRAPH http://stats.thehawken.org/graph.php?id=82&speed=6h
TARGET scale:1:/root/nms/general-rrd/rrd/82.rrd:stream0:stream1
NODES bluserv h121
VIA 1102 117
BANDWIDTH 1M 10M
LINK gamma-h121
OVERLIBGRAPH http://stats.thehawken.org/graph.php?id=78&speed=6h
TARGET scale:1:/root/nms/general-rrd/rrd/78.rrd:stream0:stream1
NODES gamma h121
VIA 1209 153
BANDWIDTH 1M
LINK bluserv-nyu
OVERLIBGRAPH http://stats.thehawken.org/graph.php?id=83&speed=6h
TARGET scale:1:/root/nms/general-rrd/rrd/83.rrd:stream0:stream1
NODES bluserv nyu
VIA 1205 148
BANDWIDTH 30M
LINK nyu-h121
OVERLIBGRAPH http://stats.thehawken.org/graph.php?id=101&speed=6h
TARGET scale:1:/root/nms/general-rrd/rrd/101.rrd:stream0:stream1
NODES nyu h121
VIA 1090 203
BANDWIDTH 1M 10M
# That's All Folks!
[edit] The Daemon
This almighty work may make this article very long, but it is here anyway.
[edit] index.php
<?php
/* Creds: Hawken / thehawken.org
* This app is a server for recieving rrd updates! :D So, enjoy! (if you know how to)
* ___
* / o o \
* | - |
* | \_/ |
* \ ___ /
*
* Short: Big eddy, unsigned, 2 bytes
* Int: Big eddy, unsigned, 4 bytes
* * * * * * * * * * * * * * * * *
* Protocol spec:
* Request:
* int -- id
* 16 byte md5 -- auth
* int -- timestamp
* short -- number of values
* strings of
* short -- length of string
* Number of bytes -- Actual string/value.
* -- One string per value.
* -- e.g. RX and TX are two values.
*
* Response:
* short -- response code
* short -- string length
* Number of bytes -- Actual string/value
* * * * * * * * * * * * * * * * *
* Responses:
* 1 - Authentication failed
* 2 - Error
* 3 - Success
*/
// Get configuration
require_once "/root/nms/general-rrd/config.php";
my_fork();
// Create socket
$msock = socket_create(AF_INET, SOCK_STREAM, 0);
socket_set_option($msock, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($msock, $listen_host, $listen_port) or die(my_log(1, "Could not bind to $listen_host:$listen_addr"));
socket_listen($msock);
my_log(4, "Master socket is ready ($listen_host:$listen_port)");
// Initialize client array
$clients = array();
// Make sql database
$sql = new sql;
$sql->init($mysql_host, $mysql_user, $mysql_pw, $mysql_db) or die(my_error(1, "Could not connect to sql"));
// Run forever
while(1){
// Gather sockets
$sel = array($msock);
$null = array();
foreach($clients as $client)$sel[]=$client->sock;
// Run select
$changed = socket_select($sel, $null, $null, NULL);
if(!$changed) mlog(3, "Wasting a cycle");
// Go through all sockets
foreach($sel as $sock){
// Identify, is this master?
if($sock == $msock){
// Then we got a new client, make a new client for that
$client = new client;
$client->sock = socket_accept($msock);
socket_getpeername($client->sock, $address, $port);
$client->address = $address;
$client->port = $port;
$clients[] = $client;
$k = array_search($client, $clients);
$client->connid = $k;
$client->sql = &$sql;
my_log(6, "Client $k connected from $address:$port.");
unset($address, $port);
// Else, identify which client sent data
} else foreach($clients as $k=>$client) if($sock == $client->sock){
$result = $client->main();
if($result === false){
socket_close($clients[$k]->sock);
unset($clients[$k]);
my_log(6, "Deleting client $k");
} else if($result === 1){
my_log(6, "Client $k requests reset (ready for new entry)");
$address = $clients[$k]->address;
$port = $clients[$k]->port;
$clients[$k] = new client;
$clients[$k]->sock = $sock; // We have $sock here! :D
$clients[$k]->connid = $k;
$clients[$k]->sql = &$sql;
$clients[$k]->address = $address;
$clients[$k]->port = $port;
unset($address, $port);
}
}
}
}
class client{
var $sock;
var $address = "0.0.0.0";
var $port = "0";
var $connid = -1;
var $stage = 0;
var $sql = false;
var $id;
var $md5;
var $num;
var $time = 0; // Unix timestamp! :D Used to identify when the values we recieved were created. Use 0 for N
var $vals = array();
var $qr = array();
var $tmplen;
var $tmperr;
function read($len){
$data = @socket_read($this->sock, $len);
if($data === false){
$err = socket_last_error($this->sock);
$str = socket_strerror($err);
$this->log(6, "Socket error $err: $str");
return false;
} else if($data==""){
$this->log(6, "Socket closed (eof)");
return false;
}
return $data;
}
function readuntil($len){
// This is STUPID! The attacker could trap the daemon inside here by sending invalid data and keeping the connection up!
$data = $this->read($len);
if($data === false) return false;
while(strlen($data) < $len){
$this->log(3, "Looping to get the specified length! (".strlen($data)."/".$len.") Oh no!!!!!");
$rest = $len - strlen($data);
$d = $this->read($rest);
if($d===false) return false;
$data .= $d;
}
return $data;
}
function prevalidate(){
$id = $this->id;
$md5 = $this->md5;
$num = $this->num;
$time = $this->time;
$sql = $this->sql;
$this->log(6, "Prevalidating");
// Get info about client
$q = 'SELECT * FROM `records` WHERE id='.$sql->escape(strval($id));
$res = $sql->query($q);
$this->qr = $qr = $sql->get($res);
$sql->free($res);
// Id exists?
if(!$qr || $qr['id'] != $id) { $this->log(6, "The client requested a non-existant ID."); return false; }
// IP filter? Port filter?
if($qr['address'] !== NULL && $qr['address'] != $this->address) { $this->log(6,"The client connects from wrong IP"); return false; }
if($qr['port'] !== NULL && $qr['port'] != $this->port) { $this->log(6, "THe client connects from wrong port"); return false; }
// md5 is correct?
if($qr['md5'] !== $md5) { $this->log(6, "The client supplied wrong password"); $this->log(6, "$md5 != {$qr["md5"]}"); return false; }
// Number of entries correct?
if($qr['num'] != $num) { $this->log(6, "The client sent an invalid number of entries."); return false; }
return true;
}
function result(){
$time = $this->time;
if($time == 0) $time = 'N';
$id = $this->id;
$file = $GLOBALS['rrddir']."/$id.rrd";
$failed = 0;
if(!file_exists($file)){
$new = true;
if(!$this->rrdcreate($file, strval($this->qr['create']))){
$this->response(2, $this->tmperr);
return;
}
}
if(!$this->rrdupdate($file, $time, $this->vals)){
$this->response(2, $this->tmperr);
return;
}
if($new) $r = "New db, successfully updated";
else $r = "Successfully updated";
$this->response(3, $r);
}
function rrdcreate($file, $line){
$this->log(4, "RRD file not found, creating $file.");
$files = escapeshellarg($file);
$this->log(6, "Cmd: rrdtool create $files $line 2>&1");
$data = `rrdtool create $files $line 2>&1`;
if(!empty($data)){
$this->tmperr = trim($data);
return false;
}
return true;
}
function rrdupdate($file, $time, $vals){
$this->log(5, "Updating $file at time $time: ".implode(", ", $vals));
$files = escapeshellarg($file);
$insert = "$time:".implode(":", $vals);
$inserts = escapeshellarg($insert);
$this->log(6, "Cmd: rrdtool update $files $inserts 2>&1");
$data = `rrdtool update $files $inserts 2>&1`;
if(!empty($data)) {
$this->tmperr = trim($data);
return false;
}
return true;
}
function main(){
// Called when there is data to read
$this->log(6, "Entering main() with \$this->stage = ".$this->stage);
$len = 0;
if($this->stage == 0){
$len = 26; // First stage, get header
$this->log(6, "Reading header, 26 bytes");
}
else if($this->stage == 1) $len = 2; // Next stage, there is a value to get, first step is to get its size
else if($this->stage == 2) $len = $this->templen; // Next stage, the length of the value is known, proceed to read out
else if($this->stage == -1) return false; // If we decided to drop the client
if($len > 0){
$data = $this->readuntil($len);
if($data === false) return false;
} else $data = "";
if($this->stage == 0){
$this->parse_header($data);
// Stage might be -1, don't return yet
} else if($this->stage == 1){
$this->templen = $this->code('short', 'decode', $data);
$this->stage = 2;
// No more work to do
return;
} else if($this->stage == 2){
$this->vals[]=$data;
$this->tmplen=0;
if(count($this->vals) < $this->num){
$this->stage = 1; // get more vals
// No more work to do
return;
} else $this->stage = 3; // Done. No extra checking, or it could potentially stick us in infinite loops
}
if($this->stage == -1) return false; // whee / fuck you
if($this->stage == 3){
$this->result();
// If stage is 3, this is a legit client. Legit clients deserve the socket to stay open :)
return 1;
}
}
function parse_header($data){
$this->log(6, "Parsing header!");
$this->id = $id = $this->code('int', 'decode', substr($data, 0, 4));
$this->md5 = $md5 = $this->code('md5', 'decode', substr($data, 4, 16));
$this->time = $time = $this->code('int', 'decode', substr($data, 20, 4));
$this->num = $num = $this->code('short', 'decode', substr($data, 24, 2));
$this->log(6, "Parsing header. id=$id, md5=$md5, time=$time, num=$num");
$ok = $this->prevalidate();
if(!$ok){
$this->stage = -1;
$this->response(1, "Authentication failed");
$this->log(3, "Authentication failed");
} else {
$this->log(6, "Authentication successful");
if($this->num > 0) $this->stage = 1;
else $this->stage = 3;
}
}
function response($code, $string){
/* short -- response code *\
|* short -- string length *|
\* Number of bytes -- Actual string/value */
$this->log(6, "Response to client: ($code) $string");
$packet = $this->code('short', 'encode', $code).$this->code('short','encode',strlen($string)).$string;
return socket_write($this->sock, $packet, strlen($packet));
}
// Returns either binary (direction: encode) or a value (direction: decode)
function code($type, $direction, $value){
if($direction=='decode'){
if($type=='int'){
$tmp = unpack('N', $value);
return $tmp[1];
} else if($type=='short'){
$tmp = unpack('n', $value);
return $tmp[1];
} else if($type=='md5'){
$ret = "";
for($i=0;$i<16;$i++){
$r = dechex(ord($value{$i}));
$ret .= str_repeat("0", 2-strlen($r)).$r;
}
return $ret;
}
} else if($direction=='encode'){
if($type=='int'){
return pack('N', $value);
} else if($type=='short'){
return pack('n', $value);
} else if($type=='md5'){
$ret = "";
for($i=0;$i<16;$i++) $ret .= chr(hexdec(substr($value, $i*2, 2)));
return $ret;
}
}
return false;
}
function log($lvl, $line){
$format = "Client ".$this->connid." ({$this->address}:{$this->port}): $line";
my_log($lvl, $format);
}
}
function my_fork(){
global $pidfile, $logstdout, $pid;
$pid = pcntl_fork();
if ($pid == -1) {
die('could not fork');
} else if ($pid) {
// parent
my_log(4, "Child process started with pid $pid");
die();
} else {
$logstdout = false;
$pid = posix_getpid();
my_log(4, "Child reporting in, pid $pid");
file_put_contents($pidfile, $pid);
return true;
}
}
function my_log($importance, $line){
global $logfile, $logstdout, $loglevel, $logmap;
if(isset($logmap[$importance])) $i = $logmap[$importance];
else $i = "Importance ".$importance;
$format = date("[r]")."Â [$i] $line\n";
if($importance <= $loglevel){
if($logstdout) echo $format;
else {
$fh = fopen($logfile, "a");
fwrite($fh, $format, strlen($format));
fclose($fh);
}
}
}
class sql {
var $host;
var $user;
var $password;
var $database;
var $link = false;
var $up = false;
function init($host, $user, $pw, $db){
$this->host = $host;
$this->user = $user;
$this->password = $pw;
$this->database = $db;
$this->link = mysql_connect($host, $user, $pw);
if($this->link === false){
$this->log(2, "Could not connect");
return false;
}
if(mysql_select_db($db, $this->link)===false){
$this->log(2, "Could not select database");
return false;
}
$this->up = true;
$this->log(4, "Successfully connected");
return true;
}
function query($sql){
$time = microtime(1);
$ret = mysql_query($sql);
if ((microtime(1)-$time)>1)
$this->log(3, 'SQL Long Query time: '.(microtime(1) - $time).' Query: '.$sql);
else if(mysql_error($this->link)){
$this->log(2, "MySQL Error: ".mysql_error()." --- SQL: ".$sql);
}
return $ret;
}
function get($resource){
return mysql_fetch_assoc($resource);
}
function free($res){
return mysql_free_result($res);
}
function num_res($res){
$this->log(1, "num_res not implemented");
die();
}
function getall($res){
$ret = array();
while($lol=$this->get($res)) $ret[]=$lol;
return $ret;
}
function escape($string){
return mysql_real_escape_string($string, $this->link);
}
function log($importance, $line){
my_log($importance, "SQL: $line");
}
}
[edit] config.php
And config.php (scraped of confidential information...
<?php
$mysql_host = "";
$mysql_db = "";
$mysql_user = "";
$mysql_pw = "";
$listen_host = "10.159.2.1";
$listen_port = 31372;
$base = "/root/nms/general-rrd";
$rrddir = $base."/rrd";
$pidfile = "$base/daemon.pid";
$logfile = "$base/daemon.log";
$logstdout = true;
$loglevel = 4;
$logmap = array(
1 => "Fatal",
2 => "Error",
3 => "Warning",
4 => "Notice",
5 => "Info",
6 => "Debug"
);