Add IP cloaking; make tor bans channel specific

This commit is contained in:
calzoneman 2014-08-14 21:42:13 -05:00
parent ecca806a58
commit 8fddbc3e6e
14 changed files with 193 additions and 142 deletions

View file

@ -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:

View file

@ -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) {

View file

@ -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,

View file

@ -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!");
}
}

View file

@ -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);
};

View file

@ -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);
}
};

View file

@ -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) {

View file

@ -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

View file

@ -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(),

View file

@ -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;
};

View file

@ -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) {

View file

@ -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(":");
}
}
})();

View file

@ -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.

View file

@ -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("");