From 8fddbc3e6ed4049126f8417786700c2f9939b616 Mon Sep 17 00:00:00 2001 From: calzoneman Date: Thu, 14 Aug 2014 21:42:13 -0500 Subject: [PATCH] Add IP cloaking; make tor bans channel specific --- config.template.yaml | 2 - lib/acp.js | 2 +- lib/channel/channel.js | 40 +++++++++-------- lib/channel/kickban.js | 54 ++++++++++++++--------- lib/channel/opts.js | 7 ++- lib/channel/poll.js | 4 +- lib/channel/voteskip.js | 4 +- lib/config.js | 1 - lib/io/ioserver.js | 82 ++++++++++++++++++++++------------- lib/{torblocker.js => tor.js} | 35 ++++++--------- lib/user.js | 33 +++++++------- lib/utilities.js | 69 ++++++++++++++++++----------- templates/channeloptions.jade | 1 + www/js/util.js | 1 + 14 files changed, 193 insertions(+), 142 deletions(-) rename lib/{torblocker.js => tor.js} (74%) diff --git a/config.template.yaml b/config.template.yaml index 3d506f6b..f148a5ca 100644 --- a/config.template.yaml +++ b/config.template.yaml @@ -115,8 +115,6 @@ max-channels-per-user: 5 max-accounts-per-ip: 5 # Minimum number of seconds between guest logins from the same IP guest-login-delay: 60 -# Block known Tor IP addresses -enable-tor-blocker: true # Configure statistics tracking stats: diff --git a/lib/acp.js b/lib/acp.js index 08933d39..b860e780 100644 --- a/lib/acp.js +++ b/lib/acp.js @@ -17,7 +17,7 @@ var Config = require("./config"); var Server = require("./server"); function eventUsername(user) { - return user.getName() + "@" + user.ip; + return user.getName() + "@" + user.realip; } function handleAnnounce(user, data) { diff --git a/lib/channel/channel.js b/lib/channel/channel.js index d3c3e812..5f0e6fed 100644 --- a/lib/channel/channel.js +++ b/lib/channel/channel.js @@ -330,10 +330,24 @@ Channel.prototype.acceptUser = function (user) { user.autoAFK(); user.socket.on("readChanLog", this.handleReadLog.bind(this, user)); - Logger.syslog.log(user.ip + " joined " + this.name); - this.logger.log("[login] Accepted connection from " + user.longip); + Logger.syslog.log(user.realip + " joined " + this.name); + if (user.socket._isUsingTor) { + if (this.modules.options && this.modules.options.get("torbanned")) { + user.kick("This channel has banned connections from Tor."); + user.socket.disconnect(true); + this.logger.log("[login] Blocked connection from Tor exit at " + + user.displayip); + return; + } + + this.logger.log("[login] Accepted connection from Tor exit at " + + user.displayip); + } else { + this.logger.log("[login] Accepted connection from " + user.displayip); + } + if (user.is(Flags.U_LOGGED_IN)) { - this.logger.log("[login] " + user.longip + " authenticated as " + user.getName()); + this.logger.log("[login] " + user.displayip + " authenticated as " + user.getName()); } var self = this; @@ -367,7 +381,7 @@ Channel.prototype.partUser = function (user) { return; } - this.logger.log("[login] " + user.longip + " (" + user.getName() + ") " + + this.logger.log("[login] " + user.displayip + " (" + user.getName() + ") " + "disconnected."); user.channel = null; /* Should be unnecessary because partUser only occurs if the socket dies */ @@ -412,7 +426,7 @@ Channel.prototype.packUserData = function (user) { muted: user.is(Flags.U_MUTED), smuted: user.is(Flags.U_SMUTED), aliases: user.account.aliases, - ip: util.maskIP(user.longip) + ip: user.displayip } }; @@ -425,7 +439,7 @@ Channel.prototype.packUserData = function (user) { muted: user.is(Flags.U_MUTED), smuted: user.is(Flags.U_SMUTED), aliases: user.account.aliases, - ip: user.ip + ip: user.realip } }; @@ -534,7 +548,7 @@ Channel.prototype.sendUserJoin = function (users, user) { user.account.aliases.join(",") + ")", 2); }; -Channel.prototype.readLog = function (shouldMaskIP, cb) { +Channel.prototype.readLog = function (cb) { var maxLen = 102400; var file = this.logger.filename; this.activeLock.lock(); @@ -558,16 +572,6 @@ Channel.prototype.readLog = function (shouldMaskIP, cb) { buffer += data; }); read.on("end", function () { - if (shouldMaskIP) { - buffer = buffer.replace( - /(?:^|\s)(\d+\.\d+\.\d+)\.\d+/g, - "$1.x" - ).replace( - /(?:^|\s)((?:[0-9a-f]+:){3}[0-9a-f]+):(?:[0-9a-f]+:){3}[0-9a-f]+/g, - "$1:x:x:x:x" - ); - } - cb(null, buffer); self.activeLock.release(); }); @@ -589,7 +593,7 @@ Channel.prototype.handleReadLog = function (user) { } var shouldMaskIP = user.account.globalRank < 255; - this.readLog(shouldMaskIP, function (err, data) { + this.readLog(function (err, data) { if (err) { user.socket.emit("readChanLog", { success: false, diff --git a/lib/channel/kickban.js b/lib/channel/kickban.js index b645a71d..268425c3 100644 --- a/lib/channel/kickban.js +++ b/lib/channel/kickban.js @@ -24,36 +24,48 @@ function KickBanModule(channel) { KickBanModule.prototype = Object.create(ChannelModule.prototype); +function checkIPBan(cname, ip, cb) { + db.channels.isIPBanned(cname, ip, function (err, banned) { + if (err) { + cb(false); + } else { + cb(banned); + } + }); +} + +function checkNameBan(cname, name, cb) { + db.channels.isNameBanned(cname, name, function (err, banned) { + if (err) { + cb(false); + } else { + cb(banned); + } + }); +} + KickBanModule.prototype.onUserPreJoin = function (user, data, cb) { if (!this.channel.is(Flags.C_REGISTERED)) { return cb(null, ChannelModule.PASSTHROUGH); } var cname = this.channel.name; - db.channels.isIPBanned(cname, user.longip, function (err, banned) { - if (err) { - cb(null, ChannelModule.PASSTHROUGH); - } else if (!banned) { - if (user.is(Flags.U_LOGGED_IN)) { - checkNameBan(); - } else { - cb(null, ChannelModule.PASSTHROUGH); - } - } else { + checkIPBan(cname, user.realip, function (banned) { + if (banned) { cb(null, ChannelModule.DENY); user.kick("Your IP address is banned from this channel."); + } else { + checkNameBan(cname, user.getName(), function (banned) { + if (banned) { + cb(null, ChannelModule.DENY); + user.kick("Your username is banned from this channel."); + } else { + cb(null, ChannelModule.PASSTHROUGH); + } + }); } }); - function checkNameBan() { - db.channels.isNameBanned(cname, user.getName(), function (err, banned) { - if (err) { - cb(null, ChannelModule.PASSTHROUGH); - } else { - cb(null, banned ? ChannelModule.DENY : ChannelModule.PASSTHROUGH); - } - }); - } }; KickBanModule.prototype.onUserPostJoin = function (user) { @@ -98,7 +110,7 @@ KickBanModule.prototype.sendBanlist = function (users) { for (var i = 0; i < banlist.length; i++) { bans.push({ id: banlist[i].id, - ip: banlist[i].ip === "*" ? "*" : util.maskIP(banlist[i].ip), + ip: banlist[i].ip === "*" ? "*" : util.cloakIP(banlist[i].ip), name: banlist[i].name, reason: banlist[i].reason, bannedby: banlist[i].bannedby @@ -381,7 +393,7 @@ KickBanModule.prototype.kickBanTarget = function (name, ip) { name = name.toLowerCase(); for (var i = 0; i < this.channel.users.length; i++) { if (this.channel.users[i].getLowerName() === name || - this.channel.users[i].longip === ip) { + this.channel.users[i].realip === ip) { this.channel.users[i].kick("You're banned!"); } } diff --git a/lib/channel/opts.js b/lib/channel/opts.js index 84950c83..d2d4f67a 100644 --- a/lib/channel/opts.js +++ b/lib/channel/opts.js @@ -22,7 +22,8 @@ function OptionsModule(channel) { show_public: false, // List the channel on the index page enable_link_regex: true, // Use the built-in link filter password: false, // Channel password (false -> no password required for entry) - allow_dupes: false // Allow duplicate videos on the playlist + allow_dupes: false, // Allow duplicate videos on the playlist + torbanned: false // Block connections from Tor exit nodes }; } @@ -245,6 +246,10 @@ OptionsModule.prototype.handleSetOptions = function (user, data) { this.opts.allow_dupes = Boolean(data.allow_dupes); } + if ("torbanned" in data && user.account.effectiveRank >= 3) { + this.opts.torbanned = Boolean(data.torbanned); + } + this.channel.logger.log("[mod] " + user.getName() + " updated channel options"); this.sendOpts(this.channel.users); }; diff --git a/lib/channel/poll.js b/lib/channel/poll.js index f6991327..274b02b6 100644 --- a/lib/channel/poll.js +++ b/lib/channel/poll.js @@ -67,7 +67,7 @@ PollModule.prototype.onUserPostJoin = function (user) { PollModule.prototype.onUserPart = function(user) { if (this.poll) { - this.poll.unvote(user.ip); + this.poll.unvote(user.realip); this.sendPollUpdate(this.channel.users); } }; @@ -142,7 +142,7 @@ PollModule.prototype.handleVote = function (user, data) { } if (this.poll) { - this.poll.vote(user.ip, data.option); + this.poll.vote(user.realip, data.option); this.sendPollUpdate(this.channel.users); } }; diff --git a/lib/channel/voteskip.js b/lib/channel/voteskip.js index cf25ded6..5d85074c 100644 --- a/lib/channel/voteskip.js +++ b/lib/channel/voteskip.js @@ -19,7 +19,7 @@ VoteskipModule.prototype.onUserPart = function(user) { return; } - this.unvote(user.ip); + this.unvote(user.realip); this.update(); }; @@ -40,7 +40,7 @@ VoteskipModule.prototype.handleVoteskip = function (user) { this.poll = new Poll("[server]", "voteskip", ["skip"], false); } - this.poll.vote(user.ip, 0); + this.poll.vote(user.realip, 0); var title = ""; if (this.channel.modules.playlist.current) { diff --git a/lib/config.js b/lib/config.js index a7d6b855..24fe7888 100644 --- a/lib/config.js +++ b/lib/config.js @@ -69,7 +69,6 @@ var defaults = { "max-channels-per-user": 5, "max-accounts-per-ip": 5, "guest-login-delay": 60, - "enable-tor-blocker": true, stats: { interval: 3600000, "max-age": 86400000 diff --git a/lib/io/ioserver.js b/lib/io/ioserver.js index aa327ed6..33c4ce44 100644 --- a/lib/io/ioserver.js +++ b/lib/io/ioserver.js @@ -11,6 +11,8 @@ var Account = require("../account"); var typecheck = require("json-typecheck"); var net = require("net"); var util = require("../utilities"); +var crypto = require("crypto"); +var isTorExit = require("../tor").isTorExit; var CONNECT_RATE = { burst: 5, @@ -43,27 +45,8 @@ function handleAuth(data, accept) { } } -/** - * Called after a connection is accepted - */ -function handleConnection(sock) { - var ip = sock.handshake.address.address; - var longip = ip; - sock._ip = ip; - if (net.isIPv6(ip)) { - longip = util.expandIPv6(ip); - } - sock._longip = longip; - var srv = Server.getServer(); - if (srv.torblocker && srv.torblocker.shouldBlockIP(ip)) { - sock.emit("kick", { - reason: "This server does not allow connections from Tor. "+ - "Please log in with your regular internet connection." - }); - Logger.syslog.log("Blocked Tor IP: " + ip); - sock.disconnect(true); - return; - } +function throttleIP(sock) { + var ip = sock._realip; if (!(ip in ipThrottle)) { ipThrottle[ip] = $util.newRateLimiter(); @@ -75,16 +58,14 @@ function handleConnection(sock) { reason: "Your IP address is connecting too quickly. Please "+ "wait 10 seconds before joining again." }); - return; + return true; } - // Check for global ban on the IP - if (db.isGlobalIPBanned(ip)) { - Logger.syslog.log("Rejecting " + ip + " - global banned"); - sock.emit("kick", { reason: "Your IP is globally banned." }); - sock.disconnect(true); - return; - } + return false; +} + +function ipLimitReached(sock) { + var ip = sock._realip; sock.on("disconnect", function () { ipCount[ip]--; @@ -106,9 +87,9 @@ function handleConnection(sock) { sock.disconnect(true); return; } +} - Logger.syslog.log("Accepted socket from " + ip); - +function addTypecheckedFunctions(sock) { sock.typecheckedOn = function (msg, template, cb) { sock.on(msg, function (data) { typecheck(data, template, function (err, data) { @@ -136,6 +117,44 @@ function handleConnection(sock) { }); }); }; +} + +/** + * Called after a connection is accepted + */ +function handleConnection(sock) { + var ip = sock.handshake.address.address; + if (net.isIPv6(ip)) { + ip = util.expandIPv6(ip); + } + sock._realip = ip; + sock._displayip = $util.cloakIP(ip); + + if (isTorExit(ip)) { + sock._isUsingTor = true; + } + + var srv = Server.getServer(); + + if (throttleIP(ip)) { + return; + } + + // Check for global ban on the IP + if (db.isGlobalIPBanned(ip)) { + Logger.syslog.log("Rejecting " + ip + " - global banned"); + sock.emit("kick", { reason: "Your IP is globally banned." }); + sock.disconnect(true); + return; + } + + if (ipLimitReached(sock)) { + return; + } + + Logger.syslog.log("Accepted socket from " + ip); + + addTypecheckedFunctions(sock); var user = new User(sock); if (sock.handshake.user) { @@ -148,6 +167,7 @@ function handleConnection(sock) { user.setFlag(Flags.U_READY); return; } + user.socket.emit("login", { success: true, name: user.getName(), diff --git a/lib/torblocker.js b/lib/tor.js similarity index 74% rename from lib/torblocker.js rename to lib/tor.js index f7b324c4..095f143c 100644 --- a/lib/torblocker.js +++ b/lib/tor.js @@ -24,8 +24,8 @@ function retrieveIPs(cb) { var d = domain.create(); d.on("error", function (err) { - if (err.trace) - Logger.errlog.log(err.trace()); + if (err.stack) + Logger.errlog.log(err.stack); else Logger.errlog.log(err); }); @@ -63,26 +63,17 @@ function getTorIPs(cb) { }); } -module.exports = function () { - var x = { - ipList: [], - shouldBlockIP: function (ip) { - return this.ipList.indexOf(ip) >= 0; - } - }; +var _ipList = []; +getTorIPs(function (err, ips) { + if (err) { + Logger.errlog.log(err); + return; + } - var init = function () { - getTorIPs(function (err, ips) { - if (err) { - Logger.errlog.log(err); - return; - } + Logger.syslog.log("Loaded Tor IP list"); + _ipList = ips; +}); - Logger.syslog.log("Loaded Tor IP list"); - x.ipList = ips; - }); - }; - - init(); - return x; +exports.isTorExit = function (ip) { + return this._ipList.indexOf(ip) >= 0; }; diff --git a/lib/user.js b/lib/user.js index fe84790b..47e455e5 100644 --- a/lib/user.js +++ b/lib/user.js @@ -14,9 +14,10 @@ function User(socket) { MakeEmitter(self); self.flags = 0; self.socket = socket; - self.ip = socket._ip; - self.longip = socket._longip; - self.account = Account.default(self.longip); + self.realip = socket._realip; + self.displayip = socket._displayip; + self.hostmask = socket._hostmask; + self.account = Account.default(self.realip); self.channel = null; self.queueLimiter = util.newRateLimiter(); self.chatLimiter = util.newRateLimiter(); @@ -65,7 +66,7 @@ function User(socket) { self.kick("Attempted initACP from non privileged user. This incident " + "will be reported."); Logger.eventlog.log("[acp] Attempted initACP from socket client " + - self.getName() + "@" + self.ip); + self.getName() + "@" + self.realip); } }); }); @@ -176,7 +177,7 @@ User.prototype.setAFK = function (afk) { if (afk) { this.setFlag(Flags.U_AFK); if (this.channel.modules.voteskip) { - this.channel.modules.voteskip.unvote(this.ip); + this.channel.modules.voteskip.unvote(this.realip); } } else { this.clearFlag(Flags.U_AFK); @@ -255,7 +256,7 @@ User.prototype.login = function (name, pw) { if (err) { if (err === "Invalid username/password combination") { Logger.eventlog.log("[loginfail] Login failed (bad password): " + name - + "@" + self.ip); + + "@" + self.realip); } self.socket.emit("login", { @@ -283,11 +284,11 @@ User.prototype.login = function (name, pw) { success: true, name: user.name }); - db.recordVisit(self.longip, self.getName()); + db.recordVisit(self.realip, self.getName()); self.socket.emit("rank", self.account.effectiveRank); - Logger.syslog.log(self.ip + " logged in as " + user.name); + Logger.syslog.log(self.realip + " logged in as " + user.name); if (self.inChannel()) { - self.channel.logger.log(self.longip + " logged in as " + user.name); + self.channel.logger.log(self.displayip + " logged in as " + user.name); } self.setFlag(Flags.U_LOGGED_IN); self.clearFlag(Flags.U_LOGGING_IN); @@ -300,8 +301,8 @@ var lastguestlogin = {}; User.prototype.guestLogin = function (name) { var self = this; - if (self.ip in lastguestlogin) { - var diff = (Date.now() - lastguestlogin[self.ip]) / 1000; + if (self.realip in lastguestlogin) { + var diff = (Date.now() - lastguestlogin[self.realip]) / 1000; if (diff < Config.get("guest-login-delay")) { self.socket.emit("login", { success: false, @@ -355,7 +356,7 @@ User.prototype.guestLogin = function (name) { } // Login succeeded - lastguestlogin[self.ip] = Date.now(); + lastguestlogin[self.realip] = Date.now(); var opts = { name: name }; if (self.inChannel()) { @@ -373,11 +374,11 @@ User.prototype.guestLogin = function (name) { name: name, guest: true }); - db.recordVisit(self.longip, self.getName()); + db.recordVisit(self.realip, self.getName()); self.socket.emit("rank", 0); - Logger.syslog.log(self.ip + " signed in as " + name); + Logger.syslog.log(self.realip + " signed in as " + name); if (self.inChannel()) { - self.channel.logger.log(self.longip + " signed in as " + name); + self.channel.logger.log(self.displayip + " signed in as " + name); } self.setFlag(Flags.U_LOGGED_IN); self.emit("login", self.account); @@ -422,7 +423,7 @@ User.prototype.refreshAccount = function (opts, cb) { opts.registered = this.is(Flags.U_REGISTERED); var self = this; var old = this.account; - Account.getAccount(name, this.longip, opts, function (err, account) { + Account.getAccount(name, this.realip, opts, function (err, account) { if (!err) { /* Update account if anything changed in the meantime */ for (var key in old) { diff --git a/lib/utilities.js b/lib/utilities.js index 9990e6f8..fa36fde1 100644 --- a/lib/utilities.js +++ b/lib/utilities.js @@ -78,31 +78,6 @@ return salt.join(''); }, - root.maskIP = function (ip) { - if (net.isIPv4(ip)) { - /* Full /32 IPv4 address */ - return ip.replace(/^(\d+\.\d+\.\d+)\.\d+/, "$1.x"); - } else if (net.isIPv4(ip + ".0")) { - /* /24 IPv4 range */ - return ip + ".0/24"; - } else if (net.isIPv4(ip + ".0.0")) { - /* /16 IPv4 widerange */ - return ip + ".0.0/16"; - } else if (net.isIPv6(ip)) { - /* /128 IPv6 address */ - return ip.replace(/^((?:[0-9a-f]+:){3}[0-9a-f]+):(?:[0-9a-f]+:){3}[0-9a-f]+$/, - "$1:x:x:x:x"); - } else if (net.isIPv6(ip + ":0:0:0:0")) { - /* /64 IPv6 range */ - return ip + "::0/64"; - } else if (net.isIPv6(ip + ":0:0:0:0:0")) { - /* /48 IPv6 widerange */ - return ip + "::0/48"; - } else { - return ip; - } - }, - root.getIPRange = function (ip) { if (net.isIPv6(ip)) { return root.expandIPv6(ip) @@ -291,4 +266,48 @@ shasum.update(data); return shasum.digest("hex"); } + + root.cloakIP = function (ip) { + if (ip.match(/\d+\.\d+(\.\d+)?(\.\d+)?/)) { + return cloakIPv4(ip); + } else if (ip.match(/([0-9a-f]{1,4}\:){1,7}[0-9a-f]{1,4}/)) { + return cloakIPv6(ip); + } else { + return ip; + } + + function iphash(ip, segment, len) { + var md5 = crypto.createHash("md5"); + md5.update(ip); + md5.update(segment); + return md5.digest("base64").substring(0, len); + } + + function cloakIPv4(ip) { + var parts = ip.split("."); + + parts = parts.map(function (segment, i) { + if (i < 2) return segment; + + return iphash(ip, segment + i, 3); + }); + + while (parts.length < 4) parts.push("*"); + return parts.join("."); + } + + function cloakIPv6(ip) { + var parts = ip.split(":"); + parts.splice(4, 4); + + parts = parts.map(function (segment, i) { + if (i < 2) return segment; + + return iphash(ip, segment + i, 4); + }); + + while (parts.length < 4) parts.push("*"); + return parts.join(":"); + } + } })(); diff --git a/templates/channeloptions.jade b/templates/channeloptions.jade index cb2913a7..c6aa231a 100644 --- a/templates/channeloptions.jade +++ b/templates/channeloptions.jade @@ -75,6 +75,7 @@ mixin adminoptions mixin textbox-auto("cs-externalcss", "External CSS", "Stylesheet URL") mixin textbox-auto("cs-externaljs", "External Javascript", "Script URL") mixin rcheckbox-auto("cs-show_public", "List channel publicly") + mixin rcheckbox-auto("cs-torbanned", "Block connections from Tor") .form-group .col-sm-8.col-sm-offset-4 span.text-info Changes are automatically saved. diff --git a/www/js/util.js b/www/js/util.js index 16294cc0..0b50521e 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -864,6 +864,7 @@ function handleModPermissions() { $("#cs-allow_voteskip").prop("checked", CHANNEL.opts.allow_voteskip); $("#cs-voteskip_ratio").val(CHANNEL.opts.voteskip_ratio); $("#cs-allow_dupes").val(CHANNEL.opts.allow_dupes); + $("#cs-torbanned").val(CHANNEL.opts.torbanned); (function() { if(typeof CHANNEL.opts.maxlength != "number") { $("#cs-maxlength").val("");