From bce71c1f986fdc944ce24373e79b9a1b555a0170 Mon Sep 17 00:00:00 2001 From: h00j Date: Sat, 13 Feb 2021 14:11:41 +0100 Subject: [PATCH] ip cloaking --- inc/bans.php | 23 ++-- inc/config.php | 10 ++ inc/functions.php | 115 ++++++++++++++++++ .../Twig/Extensions/Extension/Tinyboard.php | 2 + inc/mod/pages.php | 37 +++--- templates/mod/ban_appeals.html | 2 +- templates/mod/ban_form.html | 2 +- templates/mod/log.html | 2 +- templates/mod/report.html | 2 +- templates/mod/search_results.html | 12 +- templates/mod/user.html | 2 +- templates/mod/view_ip.html | 14 +-- templates/post/ip.html | 2 +- 13 files changed, 180 insertions(+), 45 deletions(-) diff --git a/inc/bans.php b/inc/bans.php index 05f03761..d7409aaf 100644 --- a/inc/bans.php +++ b/inc/bans.php @@ -142,6 +142,7 @@ class Bans { if ($ban['post']) $ban['post'] = json_decode($ban['post'], true); $ban['mask'] = self::range_to_string(array($ban['ipstart'], $ban['ipend'])); + $ban['cmask'] = cloak_mask($ban['mask']); $ban_list[] = $ban; } } @@ -161,8 +162,9 @@ class Bans { $end = end($bans); - foreach ($bans as &$ban) { - $ban['mask'] = self::range_to_string(array($ban['ipstart'], $ban['ipend'])); + foreach ($bans as &$ban) { + $uncloaked_mask = self::range_to_string(array($ban['ipstart'], $ban['ipend'])); + $ban['mask'] = cloak_mask($uncloaked_mask); if ($ban['post']) { $post = json_decode($ban['post']); @@ -174,11 +176,11 @@ class Bans { $ban['access'] = true; } - if (filter_var($ban['mask'], FILTER_VALIDATE_IP) !== false) { + if (filter_var($uncloaked_mask, FILTER_VALIDATE_IP) !== false) { $ban['single_addr'] = true; } if ($filter_staff || ($board_access !== false && !in_array($ban['board'], $board_access))) { - $ban['username'] = '?'; + $ban['username'] = '?'; } if ($filter_ips || ($board_access !== false && !in_array($ban['board'], $board_access))) { @list($ban['mask'], $subnet) = explode("/", $ban['mask']); @@ -200,7 +202,7 @@ class Bans { } } - $out ? fputs($out, "]") : print("]"); + $out ? fputs($out, "]") : print("]"); } @@ -230,9 +232,10 @@ class Bans { error($config['error']['noaccess']); $mask = self::range_to_string(array($ban['ipstart'], $ban['ipend'])); + $cloaked_mask = cloak_mask($mask); modLog("Removed ban #{$ban_id} for " . - (filter_var($mask, FILTER_VALIDATE_IP) !== false ? "$mask" : $mask)); + (filter_var($mask, FILTER_VALIDATE_IP) !== false ? "$cloaked_mask" : $cloaked_mask)); } query("DELETE FROM ``bans`` WHERE `id` = " . (int)$ban_id) or error(db_error()); @@ -242,7 +245,9 @@ class Bans { return true; } - static public function new_ban($mask, $reason, $length = false, $ban_board = false, $mod_id = false, $post = false) { + static public function new_ban($cloaked_mask, $reason, $length = false, $ban_board = false, $mod_id = false, $post = false) { + $mask = uncloak_mask($cloaked_mask); + global $mod, $pdo, $board; if ($mod_id === false) { @@ -251,6 +256,7 @@ class Bans { $range = self::parse_range($mask); $mask = self::range_to_string($range); + $cloaked_mask = cloak_mask($mask); $query = prepare("INSERT INTO ``bans`` VALUES (NULL, :ipstart, :ipend, :time, :expires, :board, :mod, :reason, 0, :post)"); @@ -293,14 +299,13 @@ class Bans { $query->bindValue(':post', null, PDO::PARAM_NULL); $query->execute() or error(db_error($query)); - if (isset($mod['id']) && $mod['id'] == $mod_id) { modLog('Created a new ' . ($length > 0 ? preg_replace('/^(\d+) (\w+?)s?$/', '$1-$2', until($length)) : 'permanent') . ' ban on ' . ($ban_board ? '/' . $ban_board . '/' : 'all boards') . ' for ' . - (filter_var($mask, FILTER_VALIDATE_IP) !== false ? "$mask" : $mask) . + (filter_var($mask, FILTER_VALIDATE_IP) !== false ? "$cloaked_mask" : $cloaked_mask) . ' (#' . $pdo->lastInsertId() . ')' . ' with ' . ($reason ? 'reason: ' . utf8tohtml($reason) . '' : 'no reason')); } diff --git a/inc/config.php b/inc/config.php index 6c53ce3e..78e9a03b 100644 --- a/inc/config.php +++ b/inc/config.php @@ -1874,3 +1874,13 @@ // Allowed HTML tags in ?/edit_pages. $config['allowed_html'] = 'a[href|title],p,br,li,ol,ul,strong,em,u,h2,b,i,tt,div,img[src|alt|title],hr'; + + // Secret passphrase for IP cloaking + // Disabled if empty. + $config['ipcrypt_key'] = ''; + + // IP cloak prefix + $config['ipcrypt_prefix'] = 'Cloak'; + + // Whether to append domain names to IP cloaks + $config['ipcrypt_dns'] = false; diff --git a/inc/functions.php b/inc/functions.php index 57f02ff3..22bc9a9d 100755 --- a/inc/functions.php +++ b/inc/functions.php @@ -2860,3 +2860,118 @@ function strategy_first($fun, $array) { return array('defer'); } } + +function base32_decode($d) { + $charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + $d = str_split($d); + $l = array_pop($d); + $b = ''; + foreach ($d as $c) { + $b .= sprintf("%05b", strpos($charset, $c)); + } + $padding = 8 - strlen($b) % 8; + $b .= str_pad(decbin(strpos($charset, $l)), $padding, '0', STR_PAD_LEFT); + + return implode('', array_map(function($c) { return chr(bindec($c)); }, str_split($b, 8))); +} + +function base32_encode($d) { + $charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + $b = implode('', array_map(function($c) { return sprintf("%08b", ord($c)); }, str_split($d))); + return implode('', array_map(function($c) use ($charset) { return $charset[bindec($c)]; }, str_split($b, 5))); +} + +function cloak_ip($ip) { + global $config; + $ipcrypt_key = $config['ipcrypt_key'] ?: null; + + if (empty($ipcrypt_key)) + return $ip; + + $ip_dec = inet_pton($ip); + + if ($config['ipcrypt_dns']) { + $host = gethostbyaddr($ip); + + if ($host !== $ip) { + $segments = explode('.', $host); + + $tld = []; + $tld[] = array_pop($segments); + if (count($segments) >= 2) { + $tld[] = array_pop($segments); + } + + $tld = implode('.', array_reverse($tld)); + } + } + + if (is_numeric($ip)) + $ipbytes = pack('N', $ip); + else if ($ip_dec !== false) + $ipbytes = $ip_dec; + else + return "#ERROR"; + + if (strlen($ipbytes) >= 16) + $ipbytes = substr($ipbytes, 0, 16); + + $cyphertext = openssl_encrypt($ipbytes, 'rc4-40', $ipcrypt_key, OPENSSL_RAW_DATA); + + $ret = $config['ipcrypt_prefix'].':' . base32_encode($cyphertext); + if (isset($tld) && !empty($tld)) { + $ret .= '.'.$tld; + } + + return $ret; +} + +function uncloak_ip($ip) { + global $config; + $ipcrypt_key = ($config['ipcrypt_key']); + + if (empty($ipcrypt_key)) + return $ip; + + $juice = substr($ip, strlen($config['ipcrypt_prefix']) + 1); + if ($delimiter = strpos($juice, '.')) { + $juice = substr($juice, 0, $delimiter); + } + + if (substr($ip, 0, strlen($config['ipcrypt_prefix']) + 1) === $config['ipcrypt_prefix'].':') { + $plaintext = openssl_decrypt(base32_decode($juice), 'rc4-40', $ipcrypt_key, OPENSSL_RAW_DATA); + + if ($plaintext === false || strlen($plaintext) == 0) + return '#ERROR'; + + if (strlen($ip) >= 16) + return inet_ntop($plaintext); + else + return long2ip(unpack('N', $plaintext)[1]); + } + + return '#ERROR'; +} + +function cloak_mask($mask) { + list($net, $block) = explode('/', $mask, 2); + $mask = cloak_ip($net); + if ($block) { + $mask .= '/'.$block; + } + + return $mask; +} + +function uncloak_mask($mask) { + list($addr, $block) = explode('/', $mask, 2); + $mask = uncloak_ip($addr); + if ($mask === '#ERROR') { + $mask = $addr; + } + if ($block) { + $mask .= '/'.$block; + } + + return $mask; +} diff --git a/inc/lib/Twig/Extensions/Extension/Tinyboard.php b/inc/lib/Twig/Extensions/Extension/Tinyboard.php index 3d964c71..aa529b41 100644 --- a/inc/lib/Twig/Extensions/Extension/Tinyboard.php +++ b/inc/lib/Twig/Extensions/Extension/Tinyboard.php @@ -28,6 +28,8 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension new Twig_SimpleFilter('push', 'twig_push_filter'), new Twig_SimpleFilter('bidi_cleanup', 'bidi_cleanup'), new Twig_SimpleFilter('addslashes', 'addslashes'), + new Twig_SimpleFilter('cloak_ip', 'cloak_ip'), + new Twig_SimpleFilter('cloak_mask', 'cloak_mask'), ); } diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 90180d0b..3379898a 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -773,7 +773,8 @@ function mod_view_thread50($boardName, $thread) { echo $page; } -function mod_ip_remove_note($ip, $id) { +function mod_ip_remove_note($cloaked_ip, $id) { + $ip = uncloak_ip($cloaked_ip); global $config, $mod; if (!hasPermission($config['mod']['remove_notes'])) @@ -786,13 +787,14 @@ function mod_ip_remove_note($ip, $id) { $query->bindValue(':ip', $ip); $query->bindValue(':id', $id); $query->execute() or error(db_error($query)); + + modLog("Removed a note for {$cloaked_ip}"); - modLog("Removed a note for {$ip}"); - - header('Location: ?/IP/' . $ip . '#notes', true, $config['redirect_http']); + header('Location: ?/IP/' . $cloaked_ip . '#notes', true, $config['redirect_http']); } -function mod_page_ip($ip) { +function mod_page_ip($cip) { + $ip = uncloak_ip($cip); global $config, $mod; if (filter_var($ip, FILTER_VALIDATE_IP) === false) @@ -804,7 +806,7 @@ function mod_page_ip($ip) { Bans::delete($_POST['ban_id'], true, $mod['boards']); - header('Location: ?/IP/' . $ip . '#bans', true, $config['redirect_http']); + header('Location: ?/IP/' . $cip . '#bans', true, $config['redirect_http']); return; } @@ -821,9 +823,9 @@ function mod_page_ip($ip) { $query->bindValue(':body', $_POST['note']); $query->execute() or error(db_error($query)); - modLog("Added a note for {$ip}"); + modLog("Added a note for {$cip}"); - header('Location: ?/IP/' . $ip . '#notes', true, $config['redirect_http']); + header('Location: ?/IP/' . $cip . '#notes', true, $config['redirect_http']); return; } @@ -831,7 +833,7 @@ function mod_page_ip($ip) { $args['ip'] = $ip; $args['posts'] = array(); - if ($config['mod']['dns_lookup']) + if ($config['mod']['dns_lookup'] && empty($config['ipcrypt_key'])) $args['hostname'] = rDNS($ip); $boards = listBoards(); @@ -873,16 +875,16 @@ function mod_page_ip($ip) { if (hasPermission($config['mod']['modlog_ip'])) { $query = prepare("SELECT `username`, `mod`, `ip`, `board`, `time`, `text` FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `text` LIKE :search ORDER BY `time` DESC LIMIT 50"); - $query->bindValue(':search', '%' . $ip . '%'); + $query->bindValue(':search', '%' . $cip . '%'); $query->execute() or error(db_error($query)); $args['logs'] = $query->fetchAll(PDO::FETCH_ASSOC); } else { $args['logs'] = array(); } - $args['security_token'] = make_secure_link_token('IP/' . $ip); + $args['security_token'] = make_secure_link_token('IP/' . $cip); - mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($ip)), 'mod/view_ip.html', $args, $args['hostname']); + mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($cip)), 'mod/view_ip.html', $args, $args['hostname'] ?? null); } function mod_ban() { @@ -974,7 +976,7 @@ function mod_ban_appeals() { error(_('Ban appeal not found!')); } - $ban['mask'] = Bans::range_to_string(array($ban['ipstart'], $ban['ipend'])); + $ban['mask'] = cloak_mask(Bans::range_to_string(array($ban['ipstart'], $ban['ipend']))); if (isset($_POST['unban'])) { modLog('Accepted ban appeal #' . $ban['id'] . ' for ' . $ban['mask']); @@ -1751,7 +1753,8 @@ function mod_deletebyip($boardName, $post, $global = false) { } // Record the action - modLog("Deleted all posts by IP address: $ip"); + $cip = cloak_ip($ip); + modLog("Deleted all posts by IP address: $cip"); // Redirect header('Location: ?/' . sprintf($config['board_path'], $boardName) . $config['file_index'], true, $config['redirect_http']); @@ -2210,7 +2213,7 @@ function mod_reports() { $reports = $query->fetchAll(PDO::FETCH_ASSOC); $report_queries = array(); - foreach ($reports as $report) { + foreach ($reports as &$report) { if (!isset($report_queries[$report['board']])) $report_queries[$report['board']] = array(); $report_queries[$report['board']][] = $report['post']; @@ -2308,9 +2311,9 @@ function mod_report_dismiss($id, $all = false) { } $query->execute() or error(db_error($query)); - + $cip = cloak_ip($ip); if ($all) - modLog("Dismissed all reports by $ip"); + modLog("Dismissed all reports by $cip"); else modLog("Dismissed a report for post #{$id}", $board); diff --git a/templates/mod/ban_appeals.html b/templates/mod/ban_appeals.html index 23eced12..095150c1 100644 --- a/templates/mod/ban_appeals.html +++ b/templates/mod/ban_appeals.html @@ -16,7 +16,7 @@ {% if mod|hasPermission(config.mod.show_ip, board.uri) %} {% trans 'IP' %} - {{ ban.mask }} + {{ ban.mask|cloak_mask }} {% endif %} diff --git a/templates/mod/ban_form.html b/templates/mod/ban_form.html index 98cc34b2..20981819 100644 --- a/templates/mod/ban_form.html +++ b/templates/mod/ban_form.html @@ -21,7 +21,7 @@ {% if not hide_ip %} - + {% else %} {% trans 'hidden' %} {% endif %} diff --git a/templates/mod/log.html b/templates/mod/log.html index a38b4af5..aae643de 100644 --- a/templates/mod/log.html +++ b/templates/mod/log.html @@ -27,7 +27,7 @@ {% if mod|hasPermission(config.mod.show_ip_modlog) %} - {{ log.ip }} + {{ log.ip|cloak_ip }} {% else %} hidden {% endif %} diff --git a/templates/mod/report.html b/templates/mod/report.html index b5392d5b..1229596a 100644 --- a/templates/mod/report.html +++ b/templates/mod/report.html @@ -7,7 +7,7 @@ {% trans 'Report date' %}: {{ report.time|date(config.post_date) }}
{% if mod|hasPermission(config.mod.show_ip, report.board) %} - {% trans 'Reported by' %}: {{ report.ip }} + {% trans 'Reported by' %}: {{ report.ip|cloak_ip }}
{% endif %} {% if mod|hasPermission(config.mod.report_dismiss, report.board) or mod|hasPermission(config.mod.report_dismiss_ip, report.board) %} diff --git a/templates/mod/search_results.html b/templates/mod/search_results.html index abaad703..781f0db5 100644 --- a/templates/mod/search_results.html +++ b/templates/mod/search_results.html @@ -21,7 +21,7 @@ {% for note in results %} - {{ note.ip }} + {{ note.ip|cloak_ip }} {% if note.username %} @@ -57,9 +57,9 @@ {% if ban.single_addr %} - {{ ban.mask }} + {{ ban.mask|cloak_mask }} {% else %} - {{ ban.mask|e }} + {{ ban.mask|cloak_mask|e }} {% endif %} @@ -148,7 +148,7 @@ {% endif %} - {{ log.ip }} + {{ log.ip|cloak_ip }} {{ log.time|ago }} @@ -210,8 +210,8 @@ {% if mod|hasPermission(config.mod.show_ip, post.board) %} - - {{ post.ip }} + + {{ post.ip|cloak_ip }} {% else %} hidden diff --git a/templates/mod/user.html b/templates/mod/user.html index 0fbddaeb..e774ded7 100644 --- a/templates/mod/user.html +++ b/templates/mod/user.html @@ -98,7 +98,7 @@ {% for log in logs %} - {{ log.ip }} + {{ log.ip|cloak_ip }} {{ log.time|ago }} diff --git a/templates/mod/view_ip.html b/templates/mod/view_ip.html index 4bacc7f6..7b6727f8 100644 --- a/templates/mod/view_ip.html +++ b/templates/mod/view_ip.html @@ -3,7 +3,7 @@ {{ config.board_abbreviation|sprintf(board_posts.board.uri) }} - - + - {{ board_posts.board.title|e }} @@ -17,7 +17,7 @@ {% set notes_on_record = 'note' ~ (notes|count != 1 ? 's' : '') ~ ' on record' %} {{ notes|count }} {% trans notes_on_record %} - + {% if notes|count > 0 %} @@ -45,7 +45,7 @@ {% if mod|hasPermission(config.mod.remove_notes) %} @@ -54,7 +54,7 @@ {% endfor %}
- + [{% trans 'remove' %}]
{% endif %} - + {% if mod|hasPermission(config.mod.create_notes) %}
@@ -85,7 +85,7 @@
{% set bans_on_record = 'ban' ~ (bans|count != 1 ? 's' : '') ~ ' on record' %} {{ bans|count }} {% trans bans_on_record %} - + {% for ban in bans %} @@ -102,7 +102,7 @@ {% trans 'IP' %} - {{ ban.mask }} + {{ ban.cmask }} {% trans 'Reason' %} @@ -169,7 +169,7 @@ {% if mod|hasPermission(config.mod.ban) %}
{% trans 'New ban' %} - {% set redirect = '?/IP/' ~ ip ~ '#bans' %} + {% set redirect = '?/IP/' ~ ip|cloak_ip ~ '#bans' %} {% include 'mod/ban_form.html' %}
{% endif %} diff --git a/templates/post/ip.html b/templates/post/ip.html index 11581ff9..12782e97 100644 --- a/templates/post/ip.html +++ b/templates/post/ip.html @@ -1,3 +1,3 @@ {% if post.mod and post.mod|hasPermission(config.mod.show_ip, board.uri) %} - [{{ post.ip }}] + [{{ post.ip|cloak_ip }}] {% endif %}