VPN Week – IPSec on OpenBSD

This week I’ve spent a lot of time mucking around with IPSec VPNs. I thought I should informally document some of my settings in the hope that in a years time, when I’ve forgotten everything, I have some sort of base to build on.

OS: OpenBSD >= 3.8 / Windows 7

Protocol: IPSec

Part 1 – Common Configuration

Make sure the following are enabled (via /etc/sysctl.conf or the sysctl command)

net.inet.ip.forwarding=1
net.inet.esp.enable=1
net.inet.ah.enable=1

OpenBSD is awesome thanks to ipsecctl; a 4 line configuration file is all you need for a basic setup. But first we need to start isakmpd the IKEv1 key management daemon. As we are using ipsecctl to manage most of the setup, we use the -K option to ignore the isakmpd.policy file.

To see the log files for isakmpd use -DA=nn to set the debug level of all classes to nn (where nn is between 0 and 99; I’d suggest 50). Combine with with -d to keep the daemon running in the foreground.

isakmpd -K -DA=50 -d > /tmp/isakmpd.log 2>&1

ipsecctl is used in a similar way to everyone favorite tool pfctl. To load a configuration just run:

ipsecctl -f /etc/ipsec.conf

Don’t forget to check your firewall as well, you’ll need to open up port 500 (UDP) and if you want to see the unencrypted traffic set skip on enc0.

(TODO: I also have “pass in on $if_ext inet proto esp from any to $server_me_ext” is this actually needed?)

Part 2 – Site to Site IPSec OpenBSD <-> OpenBSD

Open up /etc/ipsec.conf with vim, and then curse and moan that OpenBSD still doesn’t include vim in a default install.

Our site-site config looks like:

ike esp from 10.10.42.0/24 to 192.168.1.0/24 \
        peer 103.103.103.103 \
        main auth hmac-sha1 enc aes \
        quick auth hmac-sha1 enc aes \
        srcid 204.204.204.204 psk "put a real pre shared key here"

Where 10.10.42.0/24 is the local internal network, 192.168.1.0/24 is the remote network, 103.103.103.103 is the remote external IP and our eternal IP is 204.204.204.204.

(TODO: Fix this to use macros and define this nicely)

All that’s left is to run ipsecctl and then replicate these settings on your other OpenBSD box (all the settings will just be reversed) and you’re done.

Part 3 – Road Warrior IPSec OpenBSD <-> Windows 7

As you can see, still super simple. We are using passive mode here so our server will not try to make a VPN connection, just listen for one.

ike passive from any to any \
        main auth hmac-sha1 enc aes group modp1024 \
        quick auth hmac-sha1 enc aes \
        psk "good pre shared secrets are important"

(TODO: from any to any, will this give access to the entire network? Wouldn’t from 10.10.42.0/24 to any be better?)

(TODO: Why do we use DH Group 2 (modep1024) here and not above?)

On the Windows side I’m using Shrew Soft’s VPN client which is not only free, but works well.

I created a new Site Configuration and used the follow settings (click the image for a full view).

Of note:

  • Disable auto configuration
  • Change authentication to mutual PSK and entered the PSK
  • Set exchange type to main
  • Set DH Exchange to Group 2 for both phase 1 and phase 2

Part 4 – Summary

As you can tell I’m still learning this myself, and hopefully I’ll come back to this is a year, call my old-self an idiot and write a far better post.

Note: I wrote this at 5am in the morning, so please excuse all the mistakes

Posted on

Code Smell

“In computer programming, code smell is any symptom in the source code of a program that possibly indicates a deeper problem.” — Wikipedia

I found this piece of code this morning, I think it counts as something gone terribly, terribly wrong:

return displayItemDetails(this.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode, 14059111);

(The code was in-line JavaScript, inserted into a onclick handler, generated in PHP)

Posted on

Firefox, proxies and DNS

Firefox by default does not use your proxy when making DNS requests. This can lead to a bit of confusion if your internal DNS servers are different from your public servers, thankfully there’s a simple fix:

  1. Navigate to: about:config
  2. Locate network.proxy.socks_remote_dns and set the value to true

Posted on

Perlbal as a reverse proxy

I recently needed to set up a new reverse proxy as an alternative to pound, and for no particularly good reason chose Perlbal.

The documentation is fairly good, but I didn’t find many (good) examples of working configurations. So I thought I’d include my very simple conf.

LOAD vhosts

# Management service via telnet
CREATE SERVICE mgmt
        SET role   = management
        SET listen = 127.0.0.1:16000
ENABLE mgmt

# Web server
CREATE POOL web
        POOL web ADD 10.10.42.41:80

# Trac server
CREATE POOL trac
        POOL trac ADD 10.10.42.42:80

CREATE SERVICE web_proxy
        SET role = reverse_proxy
        SET pool = web
ENABLE web_proxy

CREATE SERVICE trac_proxy
        SET role = reverse_proxy
        SET pool = trac
ENABLE trac_proxy

# Internally we use 'trac.internal.com' but externally it would
# be 'trac.external.com'.  So rather than creating a second
# virtual host on our trac webserver, we re-write the header
HEADER trac_proxy REMOVE Host
HEADER trac_proxy INSERT Host:trac.internal.com

# Listen on our external IP
CREATE SERVICE selector
        SET listen  = 100.110.120.130:80
        SET role    = selector
        SET plugins = vhosts

        VHOST external.com.au       = web_proxy
        VHOST www.external.com.au   = web_proxy
        VHOST trac.external.com.au  = trac_proxy
ENABLE selector

Posted on

Clevo P150HM Notes

Maybe this will help someone else with this laptop:

  • If the USB3.0 ports randomly stop working you just need to update the drivers. You can grab them from Station Drivers
  • Fn+1 Toggles the fan speed between 100% and normal

Posted on

Servers and Stuff

I’ve migrated most of my sites / services away from Slicehost this week. I guess the trigger was a combination of the Rackspace migration news, and the desire for lower latency to the server. I’ve been with Slicehost for around 3 years with an almost perfect track record (at least when I don’t let a process use up all the memory and the OOM killer).

I was originally looking at Crucial Paradigm due to lots of positive feedback and good prices. However their prices are really only good for new customers as the double RAM offer is not extended to upgrades/download of plans, which could cause some problems if you ever want to resize your VPS. They also specifically disallow game servers in thier AUP (which is a problem as I’m currently running a Minecraft server).

I tested the new iiNet VPS which is located in iiNet’s WA data centre. While the pricing is good, I wasn’t unhappy with the performance of the Virtuozzo based system (I was unable to run a Minecraft server with a single user without the load > 4.0), and the latency from Melbourne wasn’t exactly great.

I’ve now switched to MammothVPS 1 and so far it’s been a good experience. Their data centre is in Sydney and has a fairly low latency for Melbourne and Sydney users2 and their plan offerings are very flexible and are competitively priced for an Australian based server. They use Xen (like Slicehost) so there’s much less risk for over selling.

  1. Yes that’s a referral link.
  2. On a iiNet ADSL2+ connection in inner Melbourne the latency is just 23ms. A TPG ADSL2+ connection in outer Melbourne was much higher at ~60ms.

Posted on

Sources of Holy Power Generation

Note: The information present in this post is a couple of months out of date. I’ve not had the time to re-update WoL data and create new graphs, but thought it might be interesting share the data/code anyway

When choosing a Holy Paladin spec for 10 mans I was never completely sure about benefits of Tower of Radiance or Blessed Life. ToR seems like a safe choice, though the frequency that you DL/FoL your beacon seemed low. Blessed Life on the other hand was obviously a PvP talent, but it would generate free HP from some raid damage.

I set out to scrape the top 200 Paladins for each 10-N boss fights from World of Logs and record their source of HP generation. I created two graphs; one that looks at all 200 parses in general, and a second that only counts when the ability is present. e.g. Only 10 or so Paladins used CS on Cho’gall, and they generated very little HP overall, but if you just look at lose 10 parses, the average is 8. This is used as a crude way to see when certain talents are taken.

Overall

Only when abilities are present

I’ve included the python code used to generate the data for these graphs below. My general disclaimer applies (i.e. be afraid)

grab-details.py

import urllib2
import re

pages = {
    'magmaw-10-n': ['http://worldoflogs.com/rankings/players/Blackwing_Descent/Magmaw/10N/Holy_Paladin/',
                    'http://worldoflogs.com/rankings/players/Blackwing_Descent/Magmaw/10N/Holy_Paladin/?page=2',
                    'http://worldoflogs.com/rankings/players/Blackwing_Descent/Magmaw/10N/Holy_Paladin/?page=3',
                    'http://worldoflogs.com/rankings/players/Blackwing_Descent/Magmaw/10N/Holy_Paladin/?page=4',
                    'http://worldoflogs.com/rankings/players/Blackwing_Descent/Magmaw/10N/Holy_Paladin/?page=5'],

    'omnitron-10-n': ['http://worldoflogs.com/rankings/players/Blackwing_Descent/Omnitron_Defense_System/10N/Holy_Paladin/',
                      'http://worldoflogs.com/rankings/players/Blackwing_Descent/Omnitron_Defense_System/10N/Holy_Paladin/?page=2',
                      'http://worldoflogs.com/rankings/players/Blackwing_Descent/Omnitron_Defense_System/10N/Holy_Paladin/?page=3',
                      'http://worldoflogs.com/rankings/players/Blackwing_Descent/Omnitron_Defense_System/10N/Holy_Paladin/?page=4',
                      'http://worldoflogs.com/rankings/players/Blackwing_Descent/Omnitron_Defense_System/10N/Holy_Paladin/?page=5'],

    'chimaeron-10-n': ['http://worldoflogs.com/rankings/players/Blackwing_Descent/Chimaeron/10N/Holy_Paladin/',
                       'http://worldoflogs.com/rankings/players/Blackwing_Descent/Chimaeron/10N/Holy_Paladin/?page=2',
                       'http://worldoflogs.com/rankings/players/Blackwing_Descent/Chimaeron/10N/Holy_Paladin/?page=3',
                       'http://worldoflogs.com/rankings/players/Blackwing_Descent/Chimaeron/10N/Holy_Paladin/?page=4',
                       'http://worldoflogs.com/rankings/players/Blackwing_Descent/Chimaeron/10N/Holy_Paladin/?page=5'],

    'atramedes-10-n': ['http://worldoflogs.com/rankings/players/Blackwing_Descent/Atramedes/10N/Holy_Paladin/',
                       'http://worldoflogs.com/rankings/players/Blackwing_Descent/Atramedes/10N/Holy_Paladin/?page=2',
                       'http://worldoflogs.com/rankings/players/Blackwing_Descent/Atramedes/10N/Holy_Paladin/?page=3',
                       'http://worldoflogs.com/rankings/players/Blackwing_Descent/Atramedes/10N/Holy_Paladin/?page=4',
                       'http://worldoflogs.com/rankings/players/Blackwing_Descent/Atramedes/10N/Holy_Paladin/?page=5'],

    'maloriak-10-n': ['http://worldoflogs.com/rankings/players/Blackwing_Descent/Maloriak/10N/Holy_Paladin/',
                      'http://worldoflogs.com/rankings/players/Blackwing_Descent/Maloriak/10N/Holy_Paladin/?page=2',
                      'http://worldoflogs.com/rankings/players/Blackwing_Descent/Maloriak/10N/Holy_Paladin/?page=3',
                      'http://worldoflogs.com/rankings/players/Blackwing_Descent/Maloriak/10N/Holy_Paladin/?page=4',
                      'http://worldoflogs.com/rankings/players/Blackwing_Descent/Maloriak/10N/Holy_Paladin/?page=5'],

    'nefarian-10-n': ['http://worldoflogs.com/rankings/players/Blackwing_Descent/Nefarian/10N/Holy_Paladin/',
                      'http://worldoflogs.com/rankings/players/Blackwing_Descent/Nefarian/10N/Holy_Paladin/?page=2',
                      'http://worldoflogs.com/rankings/players/Blackwing_Descent/Nefarian/10N/Holy_Paladin/?page=3',
                      'http://worldoflogs.com/rankings/players/Blackwing_Descent/Nefarian/10N/Holy_Paladin/?page=4',
                      'http://worldoflogs.com/rankings/players/Blackwing_Descent/Nefarian/10N/Holy_Paladin/?page=5'],

    'halfus-10-n': ['http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Halfus_Wyrmbreaker/10N/Holy_Paladin/',
                    'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Halfus_Wyrmbreaker/10N/Holy_Paladin/?page=2',
                    'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Halfus_Wyrmbreaker/10N/Holy_Paladin/?page=3',
                    'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Halfus_Wyrmbreaker/10N/Holy_Paladin/?page=4',
                    'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Halfus_Wyrmbreaker/10N/Holy_Paladin/?page=5'],

    'valiona-10-n': ['http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Valiona_&_Theralion/10N/Holy_Paladin/',
                     'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Valiona_&_Theralion/10N/Holy_Paladin/?page=2',
                     'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Valiona_&_Theralion/10N/Holy_Paladin/?page=3',
                     'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Valiona_&_Theralion/10N/Holy_Paladin/?page=4',
                     'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Valiona_&_Theralion/10N/Holy_Paladin/?page=5'],

    'twilight-10-n': ['http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Twilight_Ascendant_Council/10N/Holy_Paladin/',
                      'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Twilight_Ascendant_Council/10N/Holy_Paladin/?page=2',
                      'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Twilight_Ascendant_Council/10N/Holy_Paladin/?page=3',
                      'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Twilight_Ascendant_Council/10N/Holy_Paladin/?page=4',
                      'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Twilight_Ascendant_Council/10N/Holy_Paladin/?page=5'],

    'chogall-10-n': ['http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Cho\'gall/10N/Holy_Paladin/',
                     'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Cho\'gall/10N/Holy_Paladin/?page=2',
                     'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Cho\'gall/10N/Holy_Paladin/?page=3',
                     'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Cho\'gall/10N/Holy_Paladin/?page=4',
                     'http://worldoflogs.com/rankings/players/Bastion_of_Twilight/Cho\'gall/10N/Holy_Paladin/?page=5'],

    'conclave-10-n': ['http://worldoflogs.com/rankings/players/Throne_of_the_4_Winds/Conclave_of_Wind/10N/Holy_Paladin/',
                      'http://worldoflogs.com/rankings/players/Throne_of_the_4_Winds/Conclave_of_Wind/10N/Holy_Paladin/?page=2',
                      'http://worldoflogs.com/rankings/players/Throne_of_the_4_Winds/Conclave_of_Wind/10N/Holy_Paladin/?page=3',
                      'http://worldoflogs.com/rankings/players/Throne_of_the_4_Winds/Conclave_of_Wind/10N/Holy_Paladin/?page=4',
                      'http://worldoflogs.com/rankings/players/Throne_of_the_4_Winds/Conclave_of_Wind/10N/Holy_Paladin/?page=5'],

    'alakir-10-n': ['http://worldoflogs.com/rankings/players/Throne_of_the_4_Winds/Al\'Akir/10N/Holy_Paladin/',
                    'http://worldoflogs.com/rankings/players/Throne_of_the_4_Winds/Al\'Akir/10N/Holy_Paladin/?page=2',
                    'http://worldoflogs.com/rankings/players/Throne_of_the_4_Winds/Al\'Akir/10N/Holy_Paladin/?page=3',
                    'http://worldoflogs.com/rankings/players/Throne_of_the_4_Winds/Al\'Akir/10N/Holy_Paladin/?page=4',
                    'http://worldoflogs.com/rankings/players/Throne_of_the_4_Winds/Al\'Akir/10N/Holy_Paladin/?page=5'],

}

def extract_player_pages(url):
    regex   = "<td><a href='(/reports/[^']+)'>([^<]+)</a></td>"
    html    = urllib2.urlopen(url).read()
    matches = re.findall(regex, html)

    return matches

def extract_player_details(report_url, player_name):
    # First load the healing summary page
    url   = 'http://worldoflogs.com' + report_url
    html  = urllib2.urlopen(url).read()
    match = re.search("<a href='(/reports/[^']+)'>" + player_name + "</a>", html)

    # now load the players healing detail page
    url   = 'http://worldoflogs.com' + match.group(1)
    html  = urllib2.urlopen(url).read()

    return html


for boss in pages:
    print boss

    for url in pages[boss]:
        reports = extract_player_pages(url)

        for report_details in reports:
            report_url         = report_details[0]
            report_player_name = report_details[1]

            print 'Extracting', report_player_name

            html = extract_player_details(report_url, report_player_name)
            f    = open('data/' + boss + '/' + report_player_name, 'w+')
            f.write(html)
            f.close()

print 'Done'

generate-report.py

import glob
import re

# Spell IDs
HOLY_SHOCK         = 20473
ETERNAL_GLORY      = 88676
TOWER_OF_RADIANCE  = 88852
BLESSED_LIFE       = 89023
PURSUIT_OF_JUSTICE = 89024
CRUSADER_STRIKE    = 35395

bosses = [
    'alakir-10-n',
    'atramedes-10-n',
    'chimaeron-10-n',
    'chogall-10-n',
    'conclave-10-n',
    'halfus-10-n',
    'magmaw-10-n',
    'maloriak-10-n',
    'nefarian-10-n',
    'omnitron-10-n',
    'twilight-10-n',
    'valiona-10-n',
]


def extract_holy_power(html):
    regex   = r"            <td class='name'><a href='/reports/[^']+' rel='spell=(\d+)' class='spell'><span [^>]+>([^<]+)</span></a>\S+\n           <td>(\d+) holy power</td>"
    matches = re.findall(regex, html)

    results = {}

    for match in matches:
        (spell_id, spell_name, count) = match
        results[int(spell_id)] = int(count)

    return results


def generate_holy_power_summary(boss):
    summary = {}

    files = glob.glob('data/' + boss + '/*')
    for file in files:
        html = open(file, 'r').read()
        results = extract_holy_power(html)

        for spell_id in results:
            count = results[spell_id]
            if not summary.has_key(spell_id):
                summary[spell_id] = []
            summary[spell_id].append(count)

    return summary


report_on = [
    {'spell_name': 'HOLY_SHOCK', 'spell_id': HOLY_SHOCK},
    {'spell_name': 'ETERNAL_GLORY', 'spell_id': ETERNAL_GLORY},
    {'spell_name': 'TOWER_OF_RADIANCE', 'spell_id': TOWER_OF_RADIANCE},
    {'spell_name': 'BLESSED_LIFE', 'spell_id': BLESSED_LIFE},
    {'spell_name': 'PURSUIT_OF_JUSTICE', 'spell_id': PURSUIT_OF_JUSTICE},
    {'spell_name': 'CRUSADER_STRIKE', 'spell_id': CRUSADER_STRIKE},
]


for boss in bosses:
    print
    print boss

    summary = generate_holy_power_summary(boss)

    if not summary.has_key(HOLY_SHOCK):
        print 'Skipping, no data'
        continue

    total_count = len(summary[HOLY_SHOCK])

    print 'Total reports', total_count

    if total_count == 0:
        continue

    for report_details in report_on:
        if not summary.has_key(report_details['spell_id']):
            print report_details['spell_name'], 0
            continue

        spell_count   = len(summary[report_details['spell_id']])
        total_average = sum(summary[report_details['spell_id']]) / total_count
        spell_average = sum(summary[report_details['spell_id']]) / spell_count

        print report_details['spell_name'], total_average

Posted on

Text Twist Bot

Yesterday I watched someone play a bit of the browser based game Text Twist. Upon trying myself I found that I was awful, so I did what any programmer would do; cheated.

Results of a few hours hacking:

Features:

  • Scan for game area and crash and burn if it’s not detected
  • (Crudely) detect what letters are shown
  • Find all 3, 4, 5 and 6 letter combinations, and then throw them against a basic word list + simple anagram lookup table
  • Send key presses to the window to (try and) solve all possible words (and overwrite any work you were doing when the window loses focus)
  • Reliably gets all but one or two words for each puzzle, leaving you plenty of time to go crazy trying to finish it

Horrible source for the curious (it should at least help anyone wondering how to send key presses, or capture the current screen in python)

TextTwistBot.py

# Text Twist Bot
# http://games.yahoo.com/game/text-twist

import win32com.client as comclt
from time import sleep

from PIL import ImageGrab
from itertools import combinations

from Board import Board
from Pixels import Pixels
from Anagrams import Anagrams


def sorted_unique_character_groups(letters, length):
    c = list(combinations(''.join(letters), length))
    c = [''.join(e) for e in c]
    c = list(set(c))
    c.sort()
    return c


anagrams = Anagrams('ispell-enwl-3.1.20/english.all')
pixels   = Pixels(ImageGrab.grab())
board    = Board(pixels)
letters  = board.get_letters()

# letters = ['d', 'e', 'l', 'a', 'p', 'd']
possible_answers = []
lengths = [3,4,5,6]

for length in lengths:
    words = sorted_unique_character_groups(letters, length)
    for word in words:
        possible_answers.extend(anagrams.find(word))

possible_answers = list(set(possible_answers))

print possible_answers


wsh = comclt.Dispatch("WScript.Shell")
wsh.AppActivate("Windows Internet Explorer")

for word in possible_answers:
    for letter in word:
        wsh.SendKeys(letter)
        sleep(0.05)
    wsh.SendKeys("\n")

print 'Done'

Pixels.py

class Pixels:
    def __init__(self, img):
        self.img = img
        self.data = list(img.getdata())
        (self.width, self.height) = img.size

    def at(self, x, y):
        offset = (y * self.width) + x
        return self.data[offset]

    def grab_area(self, x1, y1, x2, y2):
        raw = []
        for y in range(y1, y2):
            for x in range(x1, x2):
                raw.append(self.at(x, y))
        return raw

Anagrams.py

class Anagrams:
    def __init__(self, word_list):
        self._load_word_list(word_list)

    def _load_word_list(self, filename):
        print 'Loading word list...',
        lines = open(filename).readlines()
        words = [line.strip().lower() for line in lines]

        self.lookup = {}

        for word in words:
            key = self._sort_letters(word)
            if key not in self.lookup:
                self.lookup[key] = []
            if word not in self.lookup[key]:
                self.lookup[key].append(word)
        print 'Done'

    def _sort_letters(self, word):
        letters = [l for l in word]
        letters.sort()
        return ''.join(letters)

    def find(self, word):
        key = self._sort_letters(word)
        if key not in self.lookup:
            return []
        results = self.lookup[key]
        #if word in results:
        #    results.remove(word)
        return results

Board.py (be afraid)

class Board:
    def __init__(self, pixels):

        # Color of the outside border, used to find the edges of the game
        self.border_green = (204, 255, 0)

        # Bounding boxes for each letter (including the circle and background)
        # Each letter is 45x45
        # I've chopped off 8pixels from each side to remove any background
        self.letter_coords = [
            ((161+8, 178+8), (161+45-8, 178+45-8)),
            ((215+8, 178+8), (215+45-8, 178+45-8)),
            ((269+8, 178+8), (269+45-8, 178+45-8)),
            ((323+8, 178+8), (323+45-8, 178+45-8)),
            ((377+8, 178+8), (377+45-8, 178+45-8)),
            ((431+8, 178+8), (431+45-8, 178+45-8)),
        ]

        # Pixel data for each letter
        self.letter_data = {
            # Snipped 300Kb of data
            # Yikes, didn't realize it was so big
            # If anyone actually cares for the letter data, you
            # can reconstruct it from error messages.
            #
            # Format is:
            #
            # 'a': [(r, g, b), (r, g, b)....],
            # 'b': [(r, g, b), (r, g, b)....],
        }

        self.pixels = pixels
        self._get_edges()

    def _get_edges(self):
        horz = self._find_pixels(range(0, self.pixels.width),      range(0, self.pixels.height, 100), self.border_green)
        vert = self._find_pixels(range(0, self.pixels.width, 100), range(0, self.pixels.height),      self.border_green)

        left   = min([x for (x,y) in horz])
        right  = max([x for (x,y) in horz])
        top    = min([y for (x,y) in vert])
        bottom = max([y for (x,y) in vert])

        self.left   = left
        self.right  = right
        self.top    = top
        self.bottom = bottom

    def _find_pixels(self, x_range, y_range, color):
        edges = []
        for y in y_range:
            for x in x_range:
                if self.pixels.at(x, y) == color:
                    edges.append((x, y))
        return edges

    def get_letters(self):
        letters = []
        for coords in self.letter_coords:
            x1 = coords[0][0] + self.left
            x2 = coords[1][0] + self.left

            y1 = coords[0][1] + self.top
            y2 = coords[1][1] + self.top

            data = self.pixels.grab_area(x1, y1, x2, y2)

            # print 'Got data for pos', coords
            found = False
            for key in self.letter_data:
                if self.letter_data[key] == data:
                    letters.append(key)
                    found = True
                    break

            if found == False:
                print '\'?\':', data, ','
        return letters

Posted on

Tyrande's Doll and Power Auras

Quickstart guide for setting up a reminder for Tyrande’s Favorite Doll using Power Auras.

  1. Create the first effect to track the mana gained buff. There is no need to adjust any of the aura visuals here as you won’t see this effect.
  2. The effect should be activated by Buff with the name Recaptured Mana, and the tooltip should contain the string 4200
  3. Close the effect window and disable the newly created effect (shift + click). Also made a note of its ID by mousing over it.
  4. Create a second effect. This will track the cooldown on the trinket (1min) as well as reference the first effect.
  5. Chose Action Usable and enter the name Tyrande’s Favorite Doll and finally enter the ID of the first effect into the next textbox (in the example the ID is 9.
  6. Customize the visuals to suite, optionally add a sound effect, and you’re done.


Posted on

Initial Divine Guardian Tests

I been wanting to look in the effectiveness of the 20% raid wall granted by Divine Guardian (4th tier talent in a Paladin’s protection tree). To do this I wrote a very simple Python program to read the combat log, detect with DG goes up and then record all the damage that was taken while it was present on a unit, and work out how much was mitigated. Of course, as the script was hacked up, it has all sorts of limitations:

  • Ignores Divine Sacrifice
  • Ignores any other mitigation effects (Talents / Sanc / Inspiration / etc)
  • Ignores overkills
  • Undefined behavior when used by two Paladins

Below is the results for a two nights in ICC 25, while I’m sure it’s not 100% it should be a reasonable ballpark figure.

Fight Damage Mitigated via DG
Gaseous Blight 37057
Gaseous Blight 32664
Ooze Explosion/Melee 39923
Trash before Blood Queen 21145
Blood Queen (Fear/Bloodbolt) 42212
Blood Queen (Fear/Bloodbolt) 54410
Blood Queen (Fear/Bloodbolt) 17679
Blood Queen (Fear/Bloodbolt) 29315
Blood Queen (Fear/Bloodbolt) 32029
Blood Queen (Fear/Bloodbolt) 51281
Blood Queen (Fear/Bloodbolt) 34494
Blood Queen (Fear/Bloodbolt) 14201
Blood Queen (Fear/Bloodbolt) 41417
Blood Queen (Fear/Bloodbolt) 18817
Blood Queen (Fear/Bloodbolt) 35641
Blood Queen (Fear/Bloodbolt) 49571
Blood Queen (Fear/Bloodbolt) 26427
Average 34017

Posted on