From db48de8597897e5024f8e9ed5acb1a8f40748169 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Thu, 25 Jun 2020 16:27:35 +0200 Subject: [PATCH] Block infohash spammers from tracker --- server/controllers/tracker.ts | 34 ++++++++++++++++++--- server/initializers/constants.ts | 3 +- server/lib/redis.ts | 17 ++++++++++- server/tests/api/server/tracker.ts | 49 +++++++++++++++++++++--------- 4 files changed, 81 insertions(+), 22 deletions(-) diff --git a/server/controllers/tracker.ts b/server/controllers/tracker.ts index cacff36ecb8..c962fada525 100644 --- a/server/controllers/tracker.ts +++ b/server/controllers/tracker.ts @@ -1,13 +1,14 @@ -import { logger } from '../helpers/logger' +import * as bitTorrentTracker from 'bittorrent-tracker' import * as express from 'express' import * as http from 'http' -import * as bitTorrentTracker from 'bittorrent-tracker' import * as proxyAddr from 'proxy-addr' import { Server as WebSocketServer } from 'ws' +import { Redis } from '@server/lib/redis' +import { logger } from '../helpers/logger' +import { CONFIG } from '../initializers/config' import { TRACKER_RATE_LIMITS } from '../initializers/constants' import { VideoFileModel } from '../models/video/video-file' import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' -import { CONFIG } from '../initializers/config' const TrackerServer = bitTorrentTracker.Server @@ -53,7 +54,16 @@ const trackerServer = new TrackerServer({ const playlistExists = await VideoStreamingPlaylistModel.doesInfohashExist(infoHash) if (playlistExists === true) return cb() - return cb(new Error(`Unknown infoHash ${infoHash} requested by ip ${ip}`)) + cb(new Error(`Unknown infoHash ${infoHash} requested by ip ${ip}`)) + + // Close socket connection and block IP for a few time + if (params.type === 'ws') { + Redis.Instance.setTrackerBlockIP(ip) + .catch(err => logger.error('Cannot set tracker block ip.', { err })) + + // setTimeout to wait filter response + setTimeout(() => params.socket.close(), 0) + } } catch (err) { logger.error('Error in tracker filter.', { err }) return cb(err) @@ -88,7 +98,21 @@ function createWebsocketTrackerServer (app: express.Application) { server.on('upgrade', (request: express.Request, socket, head) => { if (request.url === '/tracker/socket') { - wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws, request)) + const ip = proxyAddr(request, CONFIG.TRUST_PROXY) + + Redis.Instance.doesTrackerBlockIPExist(ip) + .then(result => { + if (result === true) { + logger.debug('Blocking IP %s from tracker.', ip) + + socket.write('HTTP/1.1 403 Forbidden\r\n\r\n') + socket.destroy() + return + } + + return wss.handleUpgrade(request, socket, head, ws => wss.emit('connection', ws, request)) + }) + .catch(err => logger.error('Cannot check if tracker block ip exists.', { err })) } // Don't destroy socket, we have Socket.IO too diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index dd79c0e168a..9a262fd4b59 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -633,7 +633,8 @@ const AUDIT_LOG_FILENAME = 'peertube-audit.log' const TRACKER_RATE_LIMITS = { INTERVAL: 60000 * 5, // 5 minutes ANNOUNCES_PER_IP_PER_INFOHASH: 15, // maximum announces per torrent in the interval - ANNOUNCES_PER_IP: 30 // maximum announces for all our torrents in the interval + ANNOUNCES_PER_IP: 30, // maximum announces for all our torrents in the interval + BLOCK_IP_LIFETIME: 60000 * 10 // 10 minutes } const P2P_MEDIA_LOADER_PEER_VERSION = 2 diff --git a/server/lib/redis.ts b/server/lib/redis.ts index b4cd6f8e7e0..5313c46857a 100644 --- a/server/lib/redis.ts +++ b/server/lib/redis.ts @@ -8,7 +8,8 @@ import { USER_PASSWORD_RESET_LIFETIME, USER_PASSWORD_CREATE_LIFETIME, VIDEO_VIEW_LIFETIME, - WEBSERVER + WEBSERVER, + TRACKER_RATE_LIMITS } from '../initializers/constants' import { CONFIG } from '../initializers/config' @@ -121,6 +122,16 @@ class Redis { return this.exists(this.generateViewKey(ip, videoUUID)) } + /* ************ Tracker IP block ************ */ + + setTrackerBlockIP (ip: string) { + return this.setValue(this.generateTrackerBlockIPKey(ip), '1', TRACKER_RATE_LIMITS.BLOCK_IP_LIFETIME) + } + + async doesTrackerBlockIPExist (ip: string) { + return this.exists(this.generateTrackerBlockIPKey(ip)) + } + /* ************ API cache ************ */ async getCachedRoute (req: express.Request) { @@ -213,6 +224,10 @@ class Redis { return `views-${videoUUID}-${ip}` } + private generateTrackerBlockIPKey (ip: string) { + return `tracker-block-ip-${ip}` + } + private generateContactFormKey (ip: string) { return 'contact-form-' + ip } diff --git a/server/tests/api/server/tracker.ts b/server/tests/api/server/tracker.ts index 5b56a83bb73..4b86e0b904f 100644 --- a/server/tests/api/server/tracker.ts +++ b/server/tests/api/server/tracker.ts @@ -40,21 +40,6 @@ describe('Test tracker', function () { } }) - it('Should return an error when adding an incorrect infohash', function (done) { - this.timeout(10000) - const webtorrent = new WebTorrent() - - const torrent = webtorrent.add(badMagnet) - - torrent.on('error', done) - torrent.on('warning', warn => { - const message = typeof warn === 'string' ? warn : warn.message - if (message.includes('Unknown infoHash ')) return done() - }) - - torrent.on('done', () => done(new Error('No error on infohash'))) - }) - it('Should succeed with the correct infohash', function (done) { this.timeout(10000) const webtorrent = new WebTorrent() @@ -76,6 +61,7 @@ describe('Test tracker', function () { const errCb = () => done(new Error('Tracker is enabled')) killallServers([ server ]) + reRunServer(server, { tracker: { enabled: false } }) .then(() => { const webtorrent = new WebTorrent() @@ -96,6 +82,39 @@ describe('Test tracker', function () { }) }) + it('Should return an error when adding an incorrect infohash', function (done) { + this.timeout(20000) + + killallServers([ server ]) + + reRunServer(server) + .then(() => { + const webtorrent = new WebTorrent() + + const torrent = webtorrent.add(badMagnet) + + torrent.on('error', done) + torrent.on('warning', warn => { + const message = typeof warn === 'string' ? warn : warn.message + if (message.includes('Unknown infoHash ')) return done() + }) + + torrent.on('done', () => done(new Error('No error on infohash'))) + }) + }) + + it('Should block the IP after the failed infohash', function (done) { + const webtorrent = new WebTorrent() + + const torrent = webtorrent.add(goodMagnet) + + torrent.on('error', done) + torrent.on('warning', warn => { + const message = typeof warn === 'string' ? warn : warn.message + if (message.includes('Unsupported tracker protocol')) return done() + }) + }) + after(async function () { await cleanupTests([ server ]) })