Linux Media Server

Linux Media Server with selective VPN and Dynamic DNS

Needed a media server for household use with ability to tunnel only SOME traffic through a VPN. Using Ubuntu, Plex Media Server, OpenVPN, iptables packet marking and routing by user.

The machine itself is clearly a home server. Using an old gaming rig with AMD PhenomII x6 Black edition and 8GB ram its plenty of power for what I need. The storage situation is another story entirely. It has 4 HDDs (salvage from donated computers) with Logical Volume Management giving me a grand total of 3TB.

Ubuntu is a great platform for home server use, it’s easy to set up, reliable, and still relatively lightweight. The developers over at Plex.tv have an amazing product worth paying for, flawless organization and collection of my entire media library. With Plex I can access my libraries from anywhere in the world with nearly any platform. The biggest project with this server was selective VPN tunneling – I want some of my traffic encrypted and annonomized, but not necessarily all of it – for daemons running on specific users. The elegant solution was to flag packets from this user and use those flags to change their route. The added benefit of this solution is that by rerouting the packets if my VPN connection fails it also functions as a killswitch.

#!/bin/bash


export INTERFACE="tun0"
export VPNUSER="VPN-USER"
export LANIP="192.168.0.2/24"
export NETIF="eth0"
export SCRIPT_NAME="OpenVPN Tunnel Script"

[ "$IFACE" != "lo" ] || exit 0
[ "$IFACE" != "eth0" ] || exit 0

logger -t $SCRIPT_NAME  "Setting up packet marks"


iptables -F -t nat
iptables -F -t mangle
iptables -F -t filter

# mark packets from $VPNUSER

iptables -t mangle -A OUTPUT ! --dest $LANIP  -m owner --uid-owner $VPNUSER -j MARK --set-mark 0x1
iptables -t mangle -A OUTPUT --dest $LANIP -p udp --dport 53 -m owner --uid-owner $VPNUSER -j MARK --set-mark 0x1
iptables -t mangle -A OUTPUT --dest $LANIP -p tcp --dport 53 -m owner --uid-owner $VPNUSER -j MARK --set-mark 0x1
iptables -t mangle -A OUTPUT ! --src $LANIP -j MARK --set-mark 0x1

# allow responses

iptables -A INPUT -i $INTERFACE -m conntrack --ctstate ESTABLISHED -j ACCEPT

# block everything incoming on $INTERFACE

iptables -A INPUT -i $INTERFACE -j REJECT

# send DNS to google for $VPNUSER

iptables -t nat -A OUTPUT --dest $LANIP -p udp --dport 53  -m owner --uid-owner $VPNUSER  -j DNAT --to-destination 8.8.4.4
iptables -t nat -A OUTPUT --dest $LANIP -p tcp --dport 53  -m owner --uid-owner $VPNUSER  -j DNAT --to-destination 8.8.8.8

# let $VPNUSER access lo and $INTERFACE

iptables -A OUTPUT -o lo -m owner --uid-owner $VPNUSER -j ACCEPT
iptables -A OUTPUT -o $INTERFACE -m owner --uid-owner $VPNUSER -j ACCEPT

# all packets on $INTERFACE needs to be masqueraded

iptables -t nat -A POSTROUTING -o $INTERFACE -j MASQUERADE

# reject connections from predator ip going over $NETIF

iptables -A OUTPUT ! --src $LANIP -o $NETIF -j REJECT

logger -t $SCRIPT_NAME  "Setting up routes"

GATEWAYIP=`ifconfig $INTERFACE | egrep -o '([0-9]{1,3}\.){3}[0-9]{1,3}' | egrep -v '255|(127\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})' | tail -n1`

logger -t $SCRIPT_NAME "GATEWAYIP="$GATEWAYIP

if [[ `ip rule list | grep -c 0x1` == 0 ]]; then
    ip rule add from all fwmark 0x1 lookup $VPNUSER
fi
ip route replace default via $GATEWAYIP table $VPNUSER
ip route append default via 127.0.0.1 dev lo table $VPNUSER
ip route flush cache

iptables-save

In addition to this VPN solution I wanted to implement my own Dynamic DNS service for remote access via SSH. To do this I use the APIs from my DreamHost to change DNS entries server side. It’s quick and it’s dirty, but it works.

#!/bin/sh

# dyndns.php takes 2 arguments:

#     host    = the host we're changing, in this case home.domain.com

#     passwd  = key for verification.

#     addr    = optional value of IP we want to use in case I need to change it manually

#     comment = optional comment (shows in DreamHost panel)

RESULT=$(wget -qO- "http://MYDOMAIN.com/dyndns.php?host=home&passwd=MY_SUPER-SECRET_KEY")

# we need to log everything. We're sysadmins afterall, how else are you supposed to know whats happening?

LOGFILE="/home/jacob/dyndns.log"
TIME=$(date +"%r - %D")

echo ${TIME} " - " ${RESULT} >> ${LOGFILE}

and the PHP (acquired from multiple sources and modified to suit)

<?php
// set DH_API_KEY to your key (per https://panel.dreamhost.com/?tree=home.api)

$DH_API_KEY="MY_SUPER-SECRET_API_KEY";
// set HOSTS to contain an array of fields that can be modified

// only entries in this list will be updatable
$HOSTS=array("home","laptop");

$PASSWD="MY_SUPER-SECRET_KEY";

$APP_NAME="my-dhdyndns";
$DH_API_BASE="https://api.dreamhost.com/";

function dh_request($cmd,$aargs=false) {
	global $DH_API_BASE, $DH_API_KEY, $APP_NAME;
	$base=$API_BASE;
	$id=uniqid($APP_NAME);

	$args="?key=$DH_API_KEY&cmd=$cmd&format=json";
	$args.="&unique_id=" . uniqid($APP_NAME);
	if(is_array($aargs)) {
		foreach($aargs as $key => $val) {
			$args.="&" . urlencode($key) . "=" . urlencode($val);
		}
	}

	$url=$DH_API_BASE . $args;
	$curl_handle=curl_init();
	curl_setopt($curl_handle,CURLOPT_URL,$DH_API_BASE . $args);
	curl_setopt($curl_handle,CURLOPT_CONNECTTIMEOUT,5);
	curl_setopt($curl_handle,CURLOPT_RETURNTRANSFER,1);
	$buffer = curl_exec($curl_handle);
	curl_close($curl_handle);

	if (empty($buffer)) {
    	return(false);
	} else {
    	return(json_decode($buffer,true));
	}
}
function fail($str) {
	printf("error: %s",$str);
	exit(false);
}
function bad_input($e = "") {
	fail("invalid input\n$e\n");
}
function bad_inputH() {
	fail("host not allowed\n");
}


$passwd=$_REQUEST["passwd"];
$host=$_REQUEST["host"];
//This script can use the detected external IP or one passed through args
$addr=$_REQUEST["addr"];
$comment=$_REQUEST["comment"];

if($PASSWD != $passwd) { bad_input("p"); }
if(in_array($host,$HOSTS)) {$host = $host . ".my.dns.domain.com";} else { bad_inputH(); }

if(!$DH_API_KEY) {
	$DH_API_KEY=$_REQUEST["key"];
	if(!$DH_API_KEY) { bad_input(); }
}

//This is where we get that external IP if necessary
if(!$addr) { $addr=$_SERVER["REMOTE_ADDR"]; }

$ret=dh_request("dns-list_records");
if($ret["result"] != "success") {
	fail("failed list records\n");
	return(1);
}
$found=false;
foreach($ret["data"] as $key => $row) {
	if($row["record"] == $host) {
		if($row["editable"] == 0) {
			fail("error: $host not editable");
		}
		if($row["type"] != "A") {
			fail("error: $host not a A record");
		}
		$found=$row;
	}
}

if($found) {
	if($addr==$found["value"]) {
		printf("record correct: %s => %s\n", $found["record"], $addr);
		return(0);
	}
	$ret=dh_request("dns-remove_record",
		            array("record" => $found["record"],
					      "type" => $found["type"],
						  "value" => $found["value"]));
	if($ret["result"] != "success") {
		fail("failed to remove record\n");
		return(1);
	}
	$record=$found["record"];
	$type=$found["type"];
	printf("deleted %s. had value %s\n", $record,$found["value"]);
} else {
	$record=$host;
	$type='A';
}

$ret=dh_request("dns-add_record",
                array("record" => $record,
                      "type" => $type,
                      "value" => $addr,
					  "comment" => $comment));

if($ret["result"] != "success") {
	fail(sprintf("%s\n\t%s\n",
	             "failed to add $record of type $type to $addr", $ret["data"]));
	return(1);
}

printf("success: set %s to %s\n", $record, $addr);
?>

Running this script every 24 hours and after reboots ensures I always have access to my server worldwide.

Phone

(505) 450-5744

Location

Rio Rancho, NM 87124
United States of America