-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
follow_dhcp
executable file
·207 lines (182 loc) · 6.77 KB
/
follow_dhcp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
#!/usr/bin/env php
<?php
// Parse command line arguments
$options = getopt('ods:', [
'database:',
'debug',
'oneshot',
'test',
'source:',
]);
// Process data up to date (--oneshot) or follow log continuously (no arg)
$follow = !array_key_exists('oneshot', $options) && !array_key_exists('o', $options);
$debug = array_key_exists('debug', $options) || array_key_exists('d', $options);
$source = @$options['s'] ?: @$options['source'] ?: 'dnsmasq';
// Simulation mode with a fifo
preg_match('/^mock-(.*)/', $source, $matches);
if (!empty($matches)) {
// Rewrite source but w
$source = $matches[1];
$fifoname = tempnam("/tmp", "visitors-$source");
unlink($fifoname); // Never do this in production, may cause race condition
if (!posix_mkfifo ($fifoname, 0600)) {
print("Error creating FIFO\n");
exit(1);
}
print("Using mock fifo at $fifoname\n");
$pipe = fopen($fifoname, "rb");
} else {
// Pipe is setup later when running in real mode (not with a mock pipe)
$pipe = NULL;
}
if (!in_array($source, ['dnsmasq','windows'])) {
print("Unsupported --source option. Must be either 'dnsmasq' or 'windows'\n");
exit(1);
}
require_once(__DIR__.'/lib/common.php');
if ($debug) print_r($GLOBALS);
// Database
$cursor = new SqlVar('dnsmasq_cursor', NULL);
$state = new SqlVar('visitor_state', [
'ids' => [],
'occupied' => 0,
]);
$add_visit = $db->prepare("
INSERT INTO visit (mac, enter, leave, ip, hostname, renewals)
VALUES (:mac,:now,:now,:ip,:host,0)
");
$update_visit = $db->prepare("
UPDATE visit
SET leave=:now, renewals=renewals+1
WHERE leave>:now-:lease AND mac=:mac
");
$update_visit->bindValue('lease',$merge_window_sec);
$find_users = $db->prepare("
SELECT id, nick, max(leave) AS leave, stealth
FROM public_visit
WHERE leave > ?
GROUP BY id
ORDER BY id ASC
");
// Data source
$after = $cursor->get() === NULL ? '' : "'--after-cursor=".$cursor->get()."'";
$follow_arg = $follow ? '-f' : '';
if ($pipe === NULL) {
$pipe = popen([
'dnsmasq' => "exec journalctl -n all -u dnsmasq -o json $after $follow_arg",
'windows' => "exec journalctl -n all -u windows_dhcp -o json $after $follow_arg",
][$source], "r");
}
$min_leave = time(); // First we don't know, play sure and wait whole lease time.
if (array_key_exists('test', $options)) {
print("Test mode, stopping\n");
exit(0);
}
if (!$follow) $db->exec('BEGIN');
while (true) {
// Wait for fresh data at most to next leaver deadline. If running
// in one-shot mode, wait forever.
while (true) {
$delay = $follow ? max(0, $min_leave + $merge_window_sec - time()) : INF;
if ($debug) print("Waiting for data ".($delay === INF ? 'forever' : $delay.'s') ."...\n");
$timeout = !is_data_available($pipe, $delay);
if ($timeout) {
// First possible leaver timeout reached without getting
// new data. Time to analyze the results.
break;
}
// Data is available. Parse journalctl entry.
$line = fgets($pipe);
if ($line === FALSE) {
// Stop if EOF
break 2;
}
$vars = json_decode($line);
if ($vars === NULL) {
print("Panic: Not systemd log export format\n");
exit(1);
}
switch ($source) {
case 'dnsmasq':
// Extract payload, if any.
preg_match('/^[^ ]* DHCPACK[^ ]* ([^ ]*) ([^ ]*) ?(.*)/', $vars->MESSAGE, $matches);
if (empty($matches)) {
// This line is not interesting. Go get new
continue 2;
}
// Populate all data and execute SQL
$data = [
'now' => floor($vars->__REALTIME_TIMESTAMP / 1000000), // seconds
'mac' => strtoupper(str_replace(':', '', $matches[2])), // uppercase mac with no colons
'ip' => $matches[1],
'host' => $matches[3],
];
break;
case 'windows':
// Extract payload, if any.
preg_match('|^1[01],(../../..,..:..:..),[^,]*,([^,]*),([^\.,]*)[^,]*,([^,]*)|', $vars->MESSAGE, $matches);
if (empty($matches)) {
// This line is not interesting. Go get new
continue 2;
}
$data = [
'now' => DateTime::createFromFormat('m/d/y,H:i:s',$matches[1])->getTimestamp(),
'mac' => $matches[4],
'ip' => $matches[2],
'host' => $matches[3],
];
break;
}
if ($debug) var_dump($data);
// Transaction starts here and lasts until cursor update. If
// the script dies in a transaction, it can be safely restarted.
if ($follow) $db->exec('BEGIN');
// Try to update visit first if possible, otherwise insert new.
db_execute($update_visit, $data);
if ($db->changes() === 0) {
db_execute($add_visit, $data);
}
// Store current journal cursor position
$cursor->set($vars->__CURSOR);
if ($follow) $db->exec('END');
break; // Got new possibly relevant data, analyze it.
}
// Analyzing if anything changed. Get next potential leave time
// and get list of persons on board.
$current_time = $timeout ? time() : $data['now'];
$users_result = db_execute($find_users, [$current_time - $merge_window_sec]);
$new_state = [
'ids' => [],
'nicks' => [],
'occupied' => $state->get()['occupied'],
];
$old_min_leave = $min_leave;
$min_leave = INF;
while (($row = $users_result->fetchArray(SQLITE3_ASSOC))) {
// Grab them by the first leaver time
$min_leave = min($min_leave, $row['leave']);
array_push($new_state['ids'], $row['id']);
// Stealths users are not shown with name in Matrix etc. but
// are counted in in total count.
if (!$row['stealth']) {
array_push($new_state['nicks'], $row['nick']);
}
}
$ids_changed = $state->get()['ids'] !== $new_state['ids'];
$nicks_changed = $state->get()['nicks'] !== $new_state['nicks'];
if ($ids_changed || $nicks_changed) {
// Users changed. If it was previously empty, update time.
if ($state->get()['ids'] === []) {
$new_state['occupied'] = $min_leave;
}
// Try to find suitable timing source
$new_state['ts'] = count($state->get()['ids']) < count($new_state['ids']) ?
$data['now']: // Newcomer, use event data
$old_min_leave + $merge_window_sec; // We lost a visitor, use estimate
$state->set($new_state);
print($state->getRaw()."\n");
} else {
if ($debug) print("Checked data but no changes\n");
}
}
if (!$follow) $db->exec('END');