Skip to content

Commit

Permalink
merge iptables-fast-rebind
Browse files Browse the repository at this point in the history
  • Loading branch information
serain committed Sep 1, 2018
1 parent 77c4a13 commit 190bc60
Show file tree
Hide file tree
Showing 19 changed files with 423 additions and 78 deletions.
16 changes: 10 additions & 6 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,21 @@ services:
- mongo

api:
image: node:9.11.2-alpine
build:
context: .
dockerfile: iptables-node-alpine.Dockerfile
networks:
- dref
cap_add:
- NET_ADMIN
ports:
- 0.0.0.0:80:80
- 0.0.0.0:443:80
- 0.0.0.0:8000:80
- 0.0.0.0:8080:80
- 0.0.0.0:8888:80
- 0.0.0.0:443:443
- 0.0.0.0:8000:8000
- 0.0.0.0:8080:8080
- 0.0.0.0:8888:8888
environment:
- PORT=80
- PORT=45000
volumes:
- ./dref/api/:/src:rw
- ./dref-config.yml:/tmp/dref-config.yml:ro
Expand Down
9 changes: 9 additions & 0 deletions dref-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ general:
domain: "attacker.com"
address: "1.2.3.4"
logPort: 443
iptablesTimeout: 10000

targets:
- target: "demo"
Expand All @@ -23,3 +24,11 @@ targets:
host: "192.168.1.1"
port: 80
path: "/index.html"

- target: "fast-rebind"
script: "fast-rebind"
fastRebind: true
args:
host: "192.168.1.1"
port: 80
path: "/index.html"
31 changes: 30 additions & 1 deletion dref/api/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import logger from 'morgan'
import mongoose from 'mongoose'
import YAML from 'yamljs'
import cors from 'cors'
import * as iptables from './utils/iptables'

/**
* Mongo
Expand Down Expand Up @@ -32,7 +33,8 @@ for (let i = 0; i < global.config.targets.length; i++) {
let doc = {
target: global.config.targets[i].target,
script: global.config.targets[i].script,
hang: global.config.targets[i].hang,
hang: global.config.targets[i].hang || false,
fastRebind: global.config.targets[i].fastRebind || false,
args: global.config.targets[i].args
}

Expand All @@ -41,13 +43,38 @@ for (let i = 0; i < global.config.targets.length; i++) {
})
}

/**
* Set up default iptable rules to forward all ports to the API
*/
iptables.execute({
table: iptables.Table.NAT,
command: iptables.Command.INSERT,
chain: iptables.Chain.PREROUTING,
target: iptables.Target.REDIRECT,
toPort: process.env.PORT || '3000'
})

/**
* Set up default iptable rules to REJECT all traffic to dport 1
* This is used for denying traffic and causing a fast-rebind, when configured
* (the /iptables route will forward denied traffic to dport 1 on rebind)
*/
iptables.execute({
table: iptables.Table.FILTER,
command: iptables.Command.INSERT,
chain: iptables.Chain.INPUT,
target: iptables.Target.REJECT,
fromPort: 1
})

/**
* Import routes
*/
import indexRouter from './routes/index'
import logsRouter from './routes/logs'
import scriptsRouter from './routes/scripts'
import aRecordsRouter from './routes/arecords'
import iptablesRouter from './routes/iptables'
import targetsRouter from './routes/targets'
import checkpointRouter from './routes/checkpoint'
import hangRouter from './routes/hang'
Expand All @@ -61,6 +88,7 @@ app.disable('x-powered-by')
app.set('etag', false)

app.options('/logs', cors())
app.options('/iptables', cors())

// view engine setup
app.set('views', path.join(__dirname, 'views'))
Expand All @@ -75,6 +103,7 @@ app.use('/', indexRouter)
app.use('/logs', logsRouter)
app.use('/scripts', scriptsRouter)
app.use('/arecords', aRecordsRouter)
app.use('/iptables', iptablesRouter)
app.use('/targets', targetsRouter)
app.use('/checkpoint', checkpointRouter)
app.use('/hang', hangRouter)
Expand Down
6 changes: 6 additions & 0 deletions dref/api/src/models/ARecord.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ var ARecordSchema = new mongoose.Schema({
rebind: {
type: Boolean,
default: false
},
// a dual entry will return two A records: the arecord.address above and the
// server's default address
dual: {
type: Boolean,
default: false
}
})

Expand Down
4 changes: 4 additions & 0 deletions dref/api/src/models/Target.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ var TargetSchema = new mongoose.Schema({
type: Boolean,
default: false
},
fastRebind: {
type: Boolean,
default: false
},
args: mongoose.Schema.Types.Mixed
}, { timestamps: true })

Expand Down
10 changes: 7 additions & 3 deletions dref/api/src/routes/arecords.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import mongoose from 'mongoose'
import { Router } from 'express'
import { check, validationResult } from 'express-validator/check'
import * as iptables from '../utils/iptables'

const router = Router()
const ARecord = mongoose.model('ARecord')
Expand All @@ -9,7 +10,8 @@ const ARecord = mongoose.model('ARecord')
router.post('/', [
check('domain').matches(/^([a-zA-Z0-9][a-zA-Z0-9-_]*\.)*[a-zA-Z0-9]*[a-zA-Z0-9-_]*[[a-zA-Z0-9]+$/),
check('address').optional().matches(/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/),
check('rebind').optional().isBoolean()
check('rebind').optional().isBoolean(),
check('dual').optional().isBoolean()
], function (req, res, next) {
const errors = validationResult(req)

Expand All @@ -22,15 +24,17 @@ router.post('/', [
const record = { domain: req.body.domain }
if (typeof req.body.address !== 'undefined') record.address = req.body.address
if (typeof req.body.rebind !== 'undefined') record.rebind = req.body.rebind
if (typeof req.body.dual !== 'undefined') record.dual = req.body.dual

ARecord.findOneAndUpdate({
domain: req.body.domain
}, record, { upsert: true }, function (err) {
}, record, { upsert: true, new: true }, function (err, doc) {
if (err) {
console.log(err)
return res.status(400).send()
}
res.status(204).send()

return res.status(204).send()
})
})

Expand Down
5 changes: 3 additions & 2 deletions dref/api/src/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const router = Router()
const Target = mongoose.model('Target')

router.get('/', targeter, function (req, res, next) {
Target.findOne({ target: req.target }, 'script hang args', function (err, target) {
Target.findOne({ target: req.target }, 'script hang args fastRebind', function (err, target) {
if (err || !target) return res.status(400).send()

res.render('index', {
Expand All @@ -18,7 +18,8 @@ router.get('/', targeter, function (req, res, next) {
script: target.script,
domain: global.config.general.domain,
address: global.config.general.address,
logPort: global.config.general.logPort
logPort: global.config.general.logPort,
fastRebind: target.fastRebind
}
})
})
Expand Down
66 changes: 66 additions & 0 deletions dref/api/src/routes/iptables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Router } from 'express'
import { check, validationResult } from 'express-validator/check'
import cors from 'cors'
import * as iptables from '../utils/iptables'
import { resolve } from 'url';

const router = Router()

function runIPTables (command, port, address) {
return new Promise((resolve, reject) => {
iptables.execute({
table: iptables.Table.NAT,
command: command,
chain: iptables.Chain.PREROUTING,
target: iptables.Target.REDIRECT,
fromPort: port,
toPort: 1,
srcAddress: address
}).then(status => {
resolve(status)
})
})
}

router.post('/', cors(), [
check('block').optional().isBoolean(),
check('port').optional().isInt({min: 1, max: 65535})
], function (req, res, next) {
const errors = validationResult(req)

if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() })
}

console.log('dref: POST IPTables\n' + JSON.stringify(req.body, null, 4))

// Get IP address
const ipv4Match = req.ip.match(/::ffff:(\d{0,3}.\d{0,3}.\d{0,3}.\d{0,3})/)
if (!ipv4Match) {
console.log(`source IP ${req.ip} doesn't appear to be IPv4, can't manipulate iptables and fast-rebind not available`)
return res.status(400).send()
}
const ipv4 = ipv4Match[1]

// Block for 10 seconds or unblock
if (req.body.block) {
runIPTables(iptables.Command.INSERT, req.body.port, ipv4).then(status => {
// unblock after 10 seconds max (fail-safe if client forgets to unblock)
setTimeout(function () {
runIPTables(iptables.Command.DELETE, req.body.port, ipv4)
}, global.config.general.iptablesTimeout)

if (status) {
return res.status(204).send()
}
return res.status(400).send()
})
} else {
runIPTables(iptables.Command.DELETE, req.body.port, ipv4).then(status => {
if (status) return res.status(204).send()
return res.status(400).send()
})
}
})

export default router
2 changes: 2 additions & 0 deletions dref/api/src/routes/targets.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ router.post('/', [
check('target').matches(/^[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]$/),
check('script').isString(),
check('hang').optional().isBoolean(),
check('fastRebind').optional().isBoolean(),
check('args').optional()
], function (req, res, next) {
const errors = validationResult(req)
Expand All @@ -25,6 +26,7 @@ router.post('/', [
script: req.body.script
}
if (typeof req.body.hang !== 'undefined') record.hang = req.body.hang
if (typeof req.body.fastRebind !== 'undefined') record.fastRebind = req.body.fastRebind
if (typeof req.body.args !== 'undefined') record.args = req.body.args

Target.findOneAndUpdate({
Expand Down
100 changes: 100 additions & 0 deletions dref/api/src/utils/iptables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {spawn} from 'child_process'

export const Command = Object.freeze({
APPEND: '-A',
CHECK: '-C',
DELETE: '-D',
INSERT: '-I'
})

export const Target = Object.freeze({
DROP: 'DROP',
REDIRECT: 'REDIRECT',
REJECT: 'REJECT'
})

export const Table = Object.freeze({
FILTER: 'filter',
NAT: 'nat'
})

export const Chain = Object.freeze({
INPUT: 'INPUT',
PREROUTING: 'PREROUTING'
})

function getRule ({table, command, chain, target, fromPort, toPort, srcAddress}) {
fromPort = fromPort || null
toPort = toPort || null
srcAddress = srcAddress || null

let args = []
args = args.concat(['-t', table])
args = args.concat([command, chain])
args = args.concat(['-p', 'tcp'])

if (srcAddress) args = args.concat(['--src', srcAddress])
if (fromPort) args = args.concat(['--dport', fromPort])

args = args.concat(['-j', target])

if (target == Target.REJECT) args = args.concat(['--reject-with', 'tcp-reset'])
if (toPort) args = args.concat(['--to-port', toPort])

return args
}

function checkRuleExists (args) {
// returns true if the rule with args alreadys exists
return new Promise((resolve, reject) => {
const check = spawn('iptables', args)

check.on('close', (code) => {
if (code === 1) return resolve(false)
return resolve(true)
})
})
}

export function execute ({table, command, chain, target, fromPort, toPort, srcAddress} = {}) {
return new Promise((resolve, reject) => {
fromPort = fromPort || null
toPort = toPort || null
srcAddress = srcAddress || null

checkRuleExists(getRule({
table: table,
command: Command.CHECK,
chain: chain,
target: target,
fromPort: fromPort,
toPort: toPort,
srcAddress: srcAddress
})).then((exists) => {
if (([Command.APPEND, Command.INSERT].includes(command) && exists) || (command === Command.DELETE && !exists)) {
console.log(`ignoring execute(${table}, ${command}, ${chain}, ${target}, ${fromPort}, ${toPort}, ${srcAddress})`)
resolve(true)
}

const iptables = spawn('iptables', getRule({
table: table,
command: command,
chain: chain,
target: target,
fromPort: fromPort,
toPort: toPort,
srcAddress: srcAddress
}))

iptables.on('close', (code) => {
if (code === 0) {
console.log(`success execute(${table}, ${command}, ${chain}, ${target}, ${fromPort}, ${toPort}, ${srcAddress})`)
resolve(true)
} else {
console.log(`fail execute(${table}, ${command}, ${chain}, ${target}, ${fromPort}, ${toPort}, ${srcAddress})`)
resolve(false)
}
})
})
})
}
Loading

0 comments on commit 190bc60

Please sign in to comment.