BluVPN:Weathermap

From BluWiki
Jump to: navigation, search

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"
);
Personal tools