Friday, 15 July 2022

Invoicing a spammer

Maintaining control of my personal data is important, and companies who spam me usually end up on the receiving end of a nasty email exercising my rights under GDPR to find out what data they have about me, where they got it and to demand that they stop using it.

Back at the start of 2021, several companies who were spamming me all said that they had contracted another company to do their marketing, and all of them pointed at the same company.  I sent an email to the spam company consisting of:

  1. A "Notice Before Action" demanding that they pay damages for the misuse of my personal data.
  2. A "Subject Access Request" to find out what data they held and where they got it.
  3. A request to cease spamming me.
  4. A proposed contract, under which they would be allowed to send me further spam emails in exchange for a £30 charge per email.

They admitted fault and (after some more prodding) agreed to pay a £200 settlement.

Although I don't think that sending spam emails is an ethical business plan, I'm not going to name the spamming company because they have been pretty reasonable under the circumstances.

The email address that the spam was directed to was a business address.  However, I operate that business as a sole trader, so under the Privacy and Electronic Communications Regulations (EC Directive) 2003 (PECR), that makes it the address of an "individual subscriber", no different from a personal email address.  It is unlawful to send any unsolicited marketing email to an "individual subscriber" except in some very specific circumstances.

The spam company said that their systems had misidentified me as a limited company.  According to the Information Commissioner's Office, email addresses belonging to incorporated bodies such as limited companies are not those of "individual subscribers" and therefore out of the scope of PECR, so there is no prohibition on sending them spam.

The legislation is not quite so clear, and whether or not an address is that of an "individual subscriber" depends on things which are not discoverable by the sender.  Sending unsolicited email to anyone is a risk, since the sender can't know whether or not doing so would break the law.

I was assured that the problem had been fixed and would not reccur.

More recently, I've been receiving some spam from a number of different businesses, all sharing a few similarities:

  • The from addresses of the emails were all from domains which started with "ins." - for example, example@ins.example.com.
  • They all shared a number of identical non-standard email headers.

Many of the emails were really scammy looking - things like emails promoting Amazon Business coming from a variety of email addresses that don't appear to be associated with Amazon.

After some investigation, it became clear that these were from the same spam company - the one that, a year earlier, had admitted fault, paid me £200 and assured me it wouldn't happen again.

I identified 31 spam emails that they had sent, I'd already sent them a contract, and since 31 emails × £30 = £930, I invoiced them, fully expecting to end up in court.  The only thing that would make the emails they sent lawful was the contract, so I could conclude that they had accepted it.

What happened next really surprised me.  I quickly received an email from them admitting fault, pointing out that I had missed 5 emails and asking me to reissue the invoice for 36 emails × £30 = £1080.  So I did and they paid up immediately.

According to the spam company, they migrated to a new system, which introduced the error.  I can only assume that they never actually fixed the original problem of individuals being misidentified as limited companies and instead just added my address to a suppression list.  When they transferred to a new system, they presumably didn't transfer over their suppression list.

Thursday, 1 April 2021

When a spammer ignores court action

It is unlawful for an organisation to send unsolicited marketing email / SMS messages to personal email addresses / telephone numbers, except in some specific circumstances:

  1. If you have been a customer of the organisation that sent the marketing (or have negotiated business with them, even if you never actually bought anything in the end);
  2. If you were given a clear opportunity to opt out of marketing communications at the time your details were originally collected; and
  3. If you were given a clear opportunity to opt out of marketing communications in every such communication.

If all of the above is true, they're allowed to send you unsolicited marketing, otherwise they aren't.  The rules are different for business addresses, and I won't get into that here - this post is specifically about spam to personal addresses.  The legislation for this is Regulation 22 of The Privacy and Electronic Communications (EC Directive) Regulations 2003 (PECR), and despite having "EC Directive" in the title, these regulations still apply post-Brexit.  Ignoring the regulations is probably also considered a misuse of personal data under the General Data Protection Regulation (GDPR)

Whenever I hand over personal data, I always make sure I opt out of marketing if there is the option, so in theory I shouldn't ever get any spam.  Of course, I still get quite a bit, because no one actually bothers to comply with the regulations.  And why should they?  About the worst thing that will happen to an organisation that ignores the regulations is a sternly worded letter from the Information Commissioner's Office.

Luckily, Regulation 30 of PECR and Article 79 of GDPR both allow civil action, so I've been sending legal notices to spammers, off and on, for a few years.  Usually this gets settled out of court, occasionally it goes as far as me filing proceedings with the small claims court (whereupon the spammer usually figures out that I'm serious and settles).

This is the first time that a spammer has completely ignored legal paperwork.  It was a bit of a learning curve for me, so I thought I'd document the process.

This all started when I made a purchase from bulkpowders.co.uk and they subsequently started sending me regular spam SMS messages.  I've got a standard email that I use in these situations, which I sent to their data protection officer.  Broadly, the email contains 3 parts:

  1. A "Notice Before Action", which outlines how they have broken the law, why I think I'm entitled to damages and how much I'm asking for.  I invite their comments and any offer of settlement.  They have 14 days to respond to this.
  2. A "Subject Access Request" under Article 15 of GDPR.  This is to find out what information they have about me and I specifically ask for information about which third parties they have shared my data with.  They have one month to respond to this part.
  3. A request to opt out of further communications, together with a contract.  The contract says that I'll charge for any subsequent communications and that sending any will be deemed as acceptance of the contract terms.

In this case, I did get a reply from Bulk Powders' DPO basically saying they were innocent because they had given me an opportunity to opt out.  I replied pointing out that the only way to opt out was to read down to the 1742nd word of their privacy policy which explains how to opt out, which certainly doesn't meet the requirements of a "simple" opt out.  They also responded to the Subject Access Request.  They did, however, continue to send spam SMS messages.

For both responses, they left it until the last possible day allowed by the deadlines before responding.  I queried whether they intended to settle damages and they said they did not, so that was that and I filed proceedings with the small claims court via Money Claim Online.  From what I could tell, Bulk Powders is a trading name of Sports Supplements Limited, so that's who I filed against.

Money Claim Online is a government website that makes it very simple to file small claims proceedings.  It costs £25, which you add onto the claim so that the defendant pays it if you win.

First of all you fill in a claim with your details, the defendant's details, an explanation of the claim and the total amount being claimed (including the £25 fee).  In my case, the total included the damages, plus the cost of each subsequent SMS message that I had set out in my original email to Bulk Powders:

The court sends a notice to the defendant and the defendant is supposed to either pay up or file a defence within 2 weeks.  Sports Supplements did neither - they completely ignored the paperwork that the court had sent them, so this was never going to end well for them.  So since they hadn't disputed the claim, I could ask the court to enter a default judgement in my favour.  This is a bit confusing because, from the claimant's perspective, the paperwork just says it is a "request for judgement" rather than saying that it is actually the judgement itself, but its all done through the Money Claim Online web site:

So now, they should just pay up, right?  Except they didn't - they again ignored the court.  So how to collect the money that I'm owed?  For larger amounts, you can employ the High Court Enforcement Officers, but for smaller amounts like I'm claiming you use bailiffs.  I was pretty unsure how to do this, and some Googling turned up lots of people saying that the bailiffs were usually slow and useless.  But it actually turned out to be dead easy and quite quick .

To send the bailiffs, I again went to the claim on the Money Claim Online web site and used the "Request Warrant" link.  This costs £77, which again gets added to the claim.  So if you manage to get paid, you don't have to pay (if the bailiffs can't extract money from the defendant, you're now £25 + £77 = £102 out of pocket).  A week later I received a letter from the court saying they had received a cheque and that it would be forwarded to me after it cleared.  It took another couple of weeks before I had a cheque from the court:

So there we go, it took a bit over 5 months from the time of my initial complaint until payment by the court.  I'm not sure exactly why they paid up in the end - did the bailiff actually show up at their door (in the middle of a pandemic) demanding money?  I originally asked for a £200 settlement, but by repeatedly ignoring the problem, Bulk Powders / Sports Supplements ended up paying out £362 (I got £260, the court got £102).  I'm not really sure what they hoped to achieve by ignoring things, especially once they started getting paperwork from the court - were they expecting me not to risk £77 to engage the bailiffs?

I still have serious concerns regarding Bulk Powders' handling of personal data, and I have made a complaint to the ICO (but they usually take 4-5 months to respond to complaints).  Not only did they send unlawful marketing emails, but I know that they have passed my details onto another company, who has also used them for marketing purposes.  Bulk Powders' privacy policy explicitly says they won't do that.  (I'm taking action against the other company at the moment, so can't really comment on that until it is resolved one way or the other).

Tuesday, 9 March 2021

Making children as safe as they are offline

In a speech last week, The Information Commissioner, Elizabeth Denham said:

The internet was not designed for children, but we know the benefits of children going online. We have protections and rules for kids in the offline world – but they haven’t been translated to the online world.

— Elizabeth Denham, Information Commissioner

Neil Brown from decoded.legal posted an insightful blogpost on the perception that the internet is unregulated and dangerous, compared to the offline world.  The main thrust of the blogpost is that the offline world is not designed to be safe for unsupervised children.

The ICO's Children's Code is intended to make the internet safer for children.  This is a laudable goal, and there are certainly some parts of the code that all companies should be following to protect everyone, children and adults alike.  For example, privacy information is often quite opaque, even to adults, so the requirement to provide clear privacy information would benefit us all.

The offline world is very rarely designed for unsupervised children.  As Neil points out, even children's play areas are usually only designed to be safe for children who are under supervision.  They only prevent unsupervised children from entering by posting a sign (with complex grammar that might not be understood by children).  Washing your hands of the safety of unsupervised children by posting a similar sign on your website or app would almost certainly not be allowed under the Children's Code.

The intent of the Children's Code appears to be to make the internet safe for unsupervised children, but we don't do this in the offline world because it is usually not proportionate.

And this is the crux of the matter: its impossible to make the whole offline world safe for unsupervised children.  It would require banning essential tools, or placing huge financial burdens on vendors.  Councils would have to spend a disproportionate amount of money ensuring that unsupervised children cannot access dangerous roads.  So why do we expect to do so for the online world?

The key point is supervision: the internet should be safe for children, but we shouldn't be going to a disproportionate amount of effort to make it safe for unsupervised children.

But supervision is hard.  If your child is in the kitchen juggling knives, you'll probably notice, whereas they could be in the same room as you, doing unsafe things on their phone and you'll never notice.

Neil briefly points at a few technologies that can be used for child protection:

I use some of the measures which have come in for criticism recently — VPNs, and DNS over https — to maximise the scope of the filtering of Internet connections. More filtering, and more aggressive filtering, not less.

Indeed, I suspect that it is easier to prevent an unsupervised child from travelling to a particular place online than it is offline, if that's the path down which the responsible adult wishes to go.

— Neil Brown, decoded.legal

As Neil points out, by using a VPN you can direct your child's network traffic through system which can block access to inappropriate content and allow parents to supervise their child's online activities.  The parents can install an inspection certificate on your child's device which says "your parents' filter is allowed to decrypt and supervise this device", without allowing unauthorised decryption by others.

This means that the parents, or the child's school, can have a centralised system that they use to set parental controls and supervise the children under their care, across the whole internet.

Unfortunately, the main corporations that control online platforms have unilaterally decided that parents and schools shouldn't be allowed to supervise their children.  In 2016, Google effectively pulled the plug on inspection certificates by disabling them in all Android apps.  Facebook, Twitter and others had already disabled inspection certificates in their own apps some years before.

Without inspection certificates, fine grained filtering and supervision are off the table.  The playground has an opaque fence - you saw your child enter the playground, but you're not allowed to supervise their play.  You know the playground has a slide that is too high for a child of their age, but you're only allowed to control whether they can go into the playground, not whether or not they can go on the high slide.  Are they being bullied in the playground?  Who knows - you're not allowed to look!

There are a few technologies on the horizon which could make it even harder for parents to supervise and control their children's internet access, and historically, Google, Facebook, Twitter, et-al have imposed new privacy technologies and policies upon the public without consultation.  The problem is not the technologies themselves, but that they are unilaterally imposed on users rather than giving them the choice.  Whilst the ability to improve your own privacy is great, the decision over whether a parent can supervise their child should be made by the parents and children themselves, not by untouchable corporations.

The Children's Code does talk about parental controls and monitoring, but there is no framework or requirement to standardise them so that they can interact with a parent's centralised system.  The Children's Code's requirements will simply produce a fragmented approach.  Rather than the parent being able to set controls and supervise their child across the whole internet, they will need to log into each website and app separately.  Imagine having to log in and check separate "is my child juggling knives", "is my child playing with matches" and "is my child bullying their sibling" apps in the offline world.

Rather than demanding that all websites and apps are safe for unsupervised children, the ICO should be setting out a framework for websites and apps to interoperate with centralised systems operated by parents and schools.  They should be placing requirements on companies to consider whether their policies or technologies are detrimental to filters and supervision systems that are already in place.


Note: I am the Technical Director of Opendium, a company that specialises in network based online safety systems for UK schools.  This subject is of importance not only to parents, but to anyone or any organisation that is in a position of loco-parentis, such as schools, foster parents, etc.


Update: 12th March 2021

Neil has posted a follow-up response to this blogpost.

Firstly I'd like to say that, although Neil and I fundamentally disagree on a lot of things, it's very healthy to be having the conversation, and it underscores the fact that there is no single "one size fits all" when it comes to safeguarding children.  Everyone in a position of responsibility over children will have a different opinion on how best to protect those children, and these are the people who should be making the decisions - not governments or corporations, but parents and carers.

Also, although I certainly see technology as a very important part of online safety, I'd never advocate it as the only, or either primary, solution.  Neil is absolutely right that surveillance and supervision are not the same thing, and supervision requires carers to engage with the children and actually teach them how to be safe and to support them.  Indeed, gone are the days where schools just ticked their "online safety" box by installing a filter and letting it quietly run in the corner.  These days, schools are expected to support and teach children to be safe online.  Of course, some schools are very good whilst a few do just install a filter and treat it as a done job.  Thankfully, the inspectors are getting better at asking schools about their online safety policies.  I certainly think that there should be limits on how much carers invade children's privacy, but I also think that children can't expect absolute privacy - there's some balance to be had, and that balance isn't going to be the same for every situation.

My previous comments weren't intended as a rebuttal against Neil's original post - I saw them more as a reflection on something that I think(?) we agreed on (you should supervise children instead of trying to make the world safe for unsupervised children), but our idea of supervision obviously diverges somewhat.  I think this update is probably a rebuttal of Neil's follow up post though.

Traffic decryption

So, without further ado (quotes are from Neil's blogpost):

In most implementations, your target will never know that they are not talking directly to Facebook.

This isn't really true.  Android, for example, has a persistent notification that pops up every time you boot your device reminding you that you have authorised a third party to monitor your connection.  Its not quite as obvious in on a desktop machine, but it is certainly discoverable - clicking the padlock in Firefox clearly shows a warning.  Chrome isn't quite as good, but the information is there.  The persistent Android notification could probably be made more specific, such as telling you who you authorised to monitor your connection, rather than just that someone has been authorised.

State actors have the resources to install certificates directly in the OS's root certificate store, so there's not a lot that OS vendors can do to warn the user about that - this discussion is basically about certificates that the user has authorised themselves.

If someone has built the infrastructure to intercept and inspect your communications in this way, they can look your communications with your bank, the content of your email (and modify it!) and so on.

Entirely true, but thankfully most 5 year olds don't have bank accounts.  I think it goes without saying that how you supervise children depends on a lot of factors.  A primary factor is, of course, the child's age, and what is appropriate for a 5 year old is not appropriate for a 15 year old and certainly not appropriate for adults.  There isn't a "one size fits all" solution, so why should corporations impose one?

Walled gardens

Neil talks about using DNS whitelisting to set up a walled garden that only allows access to specific websites.  This means you to decide which websites to allow access to based entirely on their host name - the rest of the web address is encrypted.  Whilst a great idea in theory, and certainly a staple of school filtering 15 years ago, in the modern age this seems quite naive and doesn't really reflect the reality of the situation for a couple of reasons:

  1. Modern websites use resources from all over the place.  As a recent example, the government's COVID testing website uses Google's reCAPTCHA, which is hosted on www.google.com, so if you wanted to allow access to the COVID testing website, you would also need to allow access to Google web search, Google Images, Google News, Google Videos, etc.  The same is true for most websites and online services these days.  Not only does this undermine the protection of your "walled garden", but it also makes it extremely hard to actually set up the whitelist in the first place - you can't just whitelist the host name of one website, you have to figure out what other resources it needs (this usually can't be automated reliably).
  2. Harmful content is quite often stored along side safe content on the same host name.  If you're allowing access to googleusercontent.com so that various Google applications work, you're also allowing access to a lot of inappropriate content.  Since the child is probably under supervision, it may not be a big concern, but we certainly shouldn't pretend that the problem doesn't exist.

That schools are discouraged from using overly restrictive blocking policies should be an indication that a walled garden approach might do more harm than good.  Parents certainly need to make a decision as to whether its better for children to be in a very restrictive walled garden, or to be allowed to explore the internet more freely with a more dynamic system offering some protection from harmful content they might stumble across.  Again, this is a decision for the parents and carers, not for government or corporations.

Their platform, their rules

The second notion I found particularly interesting was that the private space on these companies' platforms (fixing the weaknesses in their own apps), and the operating systems they develop, should not be theirs to control, and that the decisions as to how they develop their services and products should not be theirs

Businesses, of course, have an obligation to fix security weaknesses in their own apps or platforms (although will I dispute the idea that the user making the choice to allow their communications to be decrypted by a specific party is a security weakness in the operating system).  However, where there are large sections of the population who will be negatively affected by the change, I do believe that a business has an obligation to enter into a discussion to see whether everyone can be accommodated.

Neil's opinion largely seems to be "their platform, their rules, if you don't like it go elsewhere".  But where else can users go?  There are basically 2 choices for mobile operating system:

Android phones start at about £45.  They have the aforementioned problems.

Pretty much the entire online safety sector has been asking Google for a dialogue for the last 5 years and have been roundly ignored.  I've seen numerous bug reports in the Android bug tracker, opened by online safety vendors and schools, and they have all been ignored or closed by the Android team without discussion.

In 2017, the IWF put me in contact with Katie O'Donovan, Google UK's head of Public Policy to try and open a dialogue, but Google were simply not interested in discussing the matter.  People within the Home Office have expressed similar frustrations.

So lets "go elsewhere": an old model iPhone starts from about £300 (£1000 for something more up to date).  Not everyone can afford to spend that kind of money on a phone.

As well as the cost of iPhones, I have to point at an incident that happened around 2 years ago: Apple provides a mobile device management (MDM) system, which is designed to allow businesses to manage their devices.  Parental control software was also allowed to hook into the MDM system, But then Apple changed the rules so that MDM could no long be used for parental control.  Since there was no other system that parental control software could use, there was outcry from the software vendors.  Apple ignored the vendors' concerns and banned the parental control apps from the App Store.  Only later did they reverse this decision as a result of bad press.

So there are only two mobile platforms, and they both have a history of refusing to engage with the people their decisions affect.

If what Steve means is that it should have been left to responsible adults to decide whether or not they want encryption which is MitM'able or not, they do, of course, have that choice: they are not required to let their children send traffic to Facebook or Twitter, if they don't agree with the way they operate, nor are they required to adopt the Android operating system.

The "their platform, their rules" argument could be applied anywhere: Should Facebook be absolved of any child protection obligations, because it's their platform?  Should an outdoor activity centre be absolved of health and safety obligations because it's a private location?  No, of course not - we expect private businesses, both on and offline, to adhere to various duty of care obligations.  Why should we not expect Google, Apple, Facebook, Twitter, Microsoft, etc. to have a duty of care to their users, and to undertake a proper consultation to make sure that changes they make do not undermine their users' safety?  Especially if people have been trying to make them aware of the problems for years.

The idea that we should leave private companies to do whatever they want because parents have a choice to ban their children from those platforms is ridiculous, and at odds with the government's stance with respect to Online Harms.

Surveillance companies, unilateral decisions, and consultations

Lastly, I wonder if there is a degree of double-standards at play here, in that I cannot help but wonder if the vendors of child surveillance systems operate with this degree of transparency and co-operation.

Can these vendors show that those most affected by their software — the children they surveil — were consulted?

No, almost certainly not, but I don't think this is the smoking gun of double standards that Neil wants it to be.  Certainly, as far as Opendium goes, we do not "surveil" children - we merely provide the tools for schools to safeguard the children who are under their care.  Can a CCTV camera vendor show that their customers have complied with the various laws that surround installation and operation of CCTV cameras?  Almost certainly not - in both cases, the vendor is not the company responsible for doing these things, so there is no way for them to guarantee that they have been done.

What I can say is that we do work closely with our customers, and would always advise that they must not undertake any covert monitoring.  Data protection legislation does require schools to be transparent with the children about what monitoring is being done, etc.  I'm not sure why the Information Commissioner's Office has limited the Children's Code to only online services, since much of it is equally relevant to the offline world - schools certainly should be providing clear and understandable privacy information to children.

Do children have a consequence-free option of not being subjected to these surveillance measures?

In law, the child's parents (or the people in loco-parentis) are responsible for making decisions regarding the child's safety.  Do children have a consequence-free option of not being subject to their parent's gaze while playing in the park?  Are they allowed to play in the playground without a teacher watching them?  Probably not - this is not the child's decision, because they are... a child.  It is up to their parents.

But it is certainly a discussion that a child can have with their carer.  I'm certainly aware of one case where a parent requested that their child not be monitored, and the school complied with the request (after having the parent sign a suitable waiver).  I have no idea what the legality of that situation is, given that the result might be the school failing to comply with their statutory obligations.

Anyway, that's enough for today.  As I said at the start of the update, I think these discussions are healthy and, as with politics, we're far better off having a chat about these things to try and understand the opposing point of view rather than just stand at the sidelines shouting "you're wrong". :)

Friday, 28 August 2020

Adventures in Netfilter Land

We're doing some modernisation work at the moment, and part of that is moving our products to a CentOS 8 operating system. Our Opendium UTM appliance includes a firewall with a friendly web based user interface, and the back end is built upon iptables. The end user never sees the iptables bit of course - they just set up some firewall policies based on their users, groups and rule bundles:

Deep packet inspection is done in user space, but for performance reasons most of the other decision making is implemented as iptables rules. Those rules can get pretty complicated, amounting to thousands of iptables rules.

CentOS 8 has moved away from iptables, switching instead to nftables, which is claimed to improve performance, amongst other things. This shouldn't be a big deal - there's an adaption layer to allow nftables rules to be manipulated in exactly the same way as iptables rules, so in theory no need for big changes to our software.

We have plans to move more of the decision making into user space to reduce the complexity of the in-kernel rules, but we don't want to do that right now, so being able to port over the existing system with little modification is great... Except it didn't work.

iptables configurations consist of "chains", where each chain contains a list of rules. A rule is just some criteria, and an action that will be carried out if those criteria match the network traffic. Actions can be things like "ACCEPT" and "DROP" (which allow or disallow the network traffic respectively), or can be a "go to" or "jump" action that points at another chain.

So we can view iptables configurations as a directed graph, with "chains" as vertices and "go to" and "jump" rules as edges. Cycles are not allowed.

Problem 1: "Too many links"

When I tried to load the iptables rules into the Linux kernel, they were rejected with the error "Too many links". Its not a particularly helpful error, but some poking around revealed that nftables has a fixed size stack of 16, which means that your rules can only jump between chains to a maximum depth of 16 jumps.

We do have some pretty complicated rules, but they shouldn't go more than 16 jumps deep, so what's going on?

Delving into the Kernel, we find this horribleness in nft_immediate.c and similar code in nft_lookup.c:

static int nft_immediate_validate(const struct nft_ctx *ctx,
                                  const struct nft_expr *expr,
                                  const struct nft_data **d)
{
        const struct nft_immediate_expr *priv = nft_expr_priv(expr);
        struct nft_ctx *pctx = (struct nft_ctx *)ctx;
        const struct nft_data *data;
        int err;

        if (priv->dreg != NFT_REG_VERDICT)
                return 0;

        data = &priv->data;

        switch (data->verdict.code) {
        case NFT_JUMP:
        case NFT_GOTO:
                pctx->level++;
                err = nft_chain_validate(ctx, data->verdict.chain);
                if (err < 0)
                        return err;
                pctx->level--;
                break;
        default:
                break;
        }

        return 0;
}

This is part of a validation routine that happens when any new rules are added, and is responsible for the "Too many links" error if you try to add rules with a jump depth that would exhaust the 16 frame stack.

We can see that jumps and gotos are both handled the same way - pctx->level is incremented when following either a jump or a goto.  nft_chain_validate() will return an EMLINK error if the level is 16.  This seems wrong - jumps take up stack space, but gotos don't.  Looking at the rest of the code, I can't see a reason for this, so I changed it to the following (in both files), rebuilt, and that seems to solve that problem:

switch (data->verdict.code) {
case NFT_JUMP:
        pctx->level++;
        err = nft_chain_validate(ctx, data->verdict.chain);
        if (err < 0)
                return err;
        pctx->level--;
break;
case NFT_GOTO:
        err = nft_chain_validate(ctx, data->verdict.chain);
        if (err < 0)
                return err;
        break;
default:
        break;
}

There are a couple of other obvious problems with this code which I haven't tried to fix:

  1. ctx is a constant, but the const qualifier is immediately cast away.  Why do this?  Because the author likes watching the world burn?
  2. The "level" attribute of ctx (which is supposed to be a constant) is modified.  It does get restored before the function exits, but that is neglected if there's an error.  I'm guessing that the calling functions just discard the contents of ctx if there is an error, so this probably doesn't really matter, but yuckity yuck!

Problem 2: CPU Lockup

With the above fix in place I tried again and the machine crashed - it totally vanished off the network, and the console was unresponsive and eventually started showing "kernel:watchdog: BUG: soft lockup - CPU#1 stuck for 23s!" warnings.  Great.

When a new rule set is committed, two validation routines are run by the kernel.  In nf_tables_api.c we find the nf_tables_check_loops() function, which checks the graph for cycles and rejects it if it has any; and nft_table_validate(), which calls the nft_chain_validate() stuff, mentioned above, to reject anything that would exceed the stack depth.

These functions are executed each time a change is committed.  Thankfully this is only once per commit, not once per rule - if you use the iptables-restore command, you can make multiple changes at once and just have the lot validated in one go; if you're adding rules one at a time with the iptables command then you only get to make one change at a time so the validation will be re-run for every rule you add.

Unfortunately the algorithm used by these functions is just a brute-force walk of the entire graph, potentially visiting each vertex multiple times.  The following set of rules is a pathological case (it doesn't actually do anything useful, its just a test case to demonstrate the problem):

Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
A0         all  --  0.0.0.0/0            0.0.0.0/0           [goto]

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

Chain A0 (1 references)
target     prot opt source               destination         
A1         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A1         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A1         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A1         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A1         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A1         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A1         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A1         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A1         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A1         all  --  0.0.0.0/0            0.0.0.0/0           [goto]

Chain A1 (10 references)
target     prot opt source               destination         
A2         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A2         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A2         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A2         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A2         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A2         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A2         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A2         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A2         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A2         all  --  0.0.0.0/0            0.0.0.0/0           [goto]

Chain A2 (10 references)
target     prot opt source               destination         
A3         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A3         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A3         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A3         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A3         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A3         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A3         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A3         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A3         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A3         all  --  0.0.0.0/0            0.0.0.0/0           [goto]

Chain A3 (10 references)
target     prot opt source               destination         
A4         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A4         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A4         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A4         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A4         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A4         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A4         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A4         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A4         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A4         all  --  0.0.0.0/0            0.0.0.0/0           [goto]

Chain A4 (10 references)
target     prot opt source               destination         
A5         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A5         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A5         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A5         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A5         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A5         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A5         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A5         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A5         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A5         all  --  0.0.0.0/0            0.0.0.0/0           [goto]

Chain A5 (10 references)
target     prot opt source               destination         
A6         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A6         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A6         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A6         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A6         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A6         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A6         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A6         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A6         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A6         all  --  0.0.0.0/0            0.0.0.0/0           [goto]

Chain A6 (10 references)
target     prot opt source               destination         
A7         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A7         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A7         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A7         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A7         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A7         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A7         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A7         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A7         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A7         all  --  0.0.0.0/0            0.0.0.0/0           [goto]

Chain A7 (10 references)
target     prot opt source               destination         
A8         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A8         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A8         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A8         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A8         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A8         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A8         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A8         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A8         all  --  0.0.0.0/0            0.0.0.0/0           [goto]
A8         all  --  0.0.0.0/0            0.0.0.0/0           [goto]

Chain A8 (10 references)
target     prot opt source               destination         

This is a mere 81 rules, and it takes about 5 seconds to commit them.  Adding an a single additional rule using "iptables -A INPUT -g A0" takes about another 5 seconds because it retriggers the validation, and worst of all, the CPU is locked up for the duration of the validation, leaving the machine unresponsive.

The problem gets exponentially worse, so if we add another chain and another 10 rules the machine will become unresponsive for 50 seconds.  Add another and your server will be AWOL for 8 minutes!

nft_table_validate() always walks the whole graph, nft_table_check_loops() often only walks a subsection of it, but nft_table_check_loops() is actually pretty quick even when walking over the whole graph.  nft_table_validate() is very very slow by comparison.  I couldn't immediately see why there was such a big difference in speed - they both seem to be very similar and neither really does a lot other than recursively walking across the whole graph.

What's the Solution?

The legacy iptables kernel modules still exist in CentOS 8, but the legacy user space tools aren't packaged any more.  But the legacy tools are still part of the iptables SRPM - they are even built, but then omitted from the final RPM.  It was a trivial job for me to edit the spec file, rebuild the package and continue using the legacy iptables modules rather than switching to nftables.

So in the short term, that's what we're doing.  We've got a lot of other modernisation work going on (a big overhaul of the UI and heritable policy structure) so would rather not get side tracked right now.

I've got a longer term plan to switch to using nftables natively, as well as move a lot of the decision making into user space to reduce the complexity of the rules.  We may well end up with a simple enough nftables configuration for these problems to largely go away.  Although I dislike the idea of living with a bug that could basically bring down a server if you load a particularly pathological configuration.

As mentioned above, it's not clear to me why there's a big difference in speed between nft_table_check_loops() and nft_table_validate().  There are certainly better algorithms for the validation they are doing, and even just adding some simple caching to avoid revisiting the same vertices over and over would be a big help.  I may end up revisiting this and rewriting the offending bits of the Linux kernel at some point in the future.

Bugzilla

These bugs have been submitted to the Netfilter Bugzilla at the following links:

Thursday, 5 September 2019

Thoughts on brexit and democracy

Whether you think we should leave or remain, as far as I can see these are the facts:
  1. For the past 40 years, the EU have been getting the blame for a lot of problems. Some of this is deserved, some of it is scape goating.
  2. In 2016 the Prime Minister, David Cameron, committed to doing "my best" to implement the result of the referendum.
  3. David Cameron arranged for parliament to legislate for an advisory referendum.
  4. Campaigns commenced, in which both sides lied. They stated things about our current relationship with the EU which were demonstrably untrue (or at the very least, were designed to mislead), they continued to do so after evidence was presented showing they were untrue, and they presented their predictions as fact - both sides said certain things would definitely happen if we voted to leave, and that certain things would definitely happen if we voted to remain.
  5. Several of the big campaigns broke the law (and this has since been proved in court.
  6. The referendum was held, in which 52% of voters voted to "leave" and 48% voted to "remain". Both options were unqualified as to exactly what they meant.
  7. David Cameron demonstrated what doing "my best" entailed by immediately quitting and leaving the problem to Theresa May.
  8. Parliament is made up of MPs who have direct contact with their constituents, but the government didn't try to gauge Parliament's understanding of what their constituents wanted from brexit, and therefore what Parliament were likely to support.
  9. The Conservative government interpreted the referendum result as meaning that a "hard" brexit should be implemented and started work to take us out of the single market.
  10. Parliament voted to start the process of us leaving the EU. 81% voted to start the process, 19% voted not to start the process.
  11. Despite 81% of the MPs apparently supporting leaving (at least, they voted to start the process), the government felt they didn't have a big enough majority to push through a "hard brexit", so called a general election.
  12. In 2017, the public democratically elected a new parliament, and the Conservative party lost their majority, but stayed in power as a minority government.
  13. The government's strategy to leave the EU remained unchanged despite the new mix of MPs - they were still negotiating a hard Brexit with no input from the new Parliament.  The MPs are elected to represent all of their constituents, but the government didn't consult with them to see what the constituents wanted from brexit, and therefore what Parliament would support.
  14. The government negotiated a "hard brexit" deal with the EU and then asked Parliament to support it.  Parliament refused to support it, which might not be surprising since the government had never asked if their view of brexit matched up with the views of the MPs.
  15. Courts ruled that there had been a lot of wrong-doing during the referendum, but that they couldn't invalidate the result because it had been legislated as an "advisory" referendum, despite David Cameron pledging to implement whatever the result was.  If the referendum had not been "advisory", its likely that the result would have been declared invalid and the whole thing re-run to make sure the public.
  16. There is testimony from an expert that there's good reason to believe that the illegal behaviour significantly affected the referendum result.  This is obviously expert opinion rather than provable fact.
  17. In 2019, Boris Johnson took over as Prime Minister.  One of his first acts was to prorogue (suspend) Parliament.  This would reduce the amount of time available to Parliament and likely reduce the amount of influence Parliament has over brexit.  I'm not commenting on why he is proroguing Parliament, only that he is and it has an impact.
  18. Boris Johnson has stated that he believes he will get a last minute deal from the EU, but that if he doesn't we will leave without a deal.
  19. The vast majority of MPs in Parliament have indicated that they don't support the UK leaving without a deal.
  20. The majority of MPs have indicated that they don't support risking a no-deal brexit.  There is good indication that many of them simply don't believe that the Prime Minister will get a deal.
  21. The Prime Minister reportedly hasn't actually tried to negotiate with the EU, which probably makes MPs less inclined to believe he will get a deal.
  22. Parliament have voted to prevent a no-deal brexit from happening.

Opinion

So, I've tried to be as factual as possible up until now.  I don't think you can reasonably argue that any of the facts above are untrue.  Certainly I've seen people dismiss expert opinion with comments like "oh but that expert is a remainer so their opinion doesn't count", but that doesn't change the fact that an expert has expressed an opinion.

This is where things get a bit more murky, because I'm putting my opinion forward rather than just sticking to the facts.  But I've tried to think critically and logically about this in light of all the facts.

Firstly, I think dismissing expert opinions as irrelevant because they might have some bias is a shame - all experts on both sides have valuable things to say.  But notably, most of the expert opinion I have seen is that leaving without a deal is going to cause big problems.  Conversely, the claims that everything will be fine always seem to be made by people who are not experts in the relevant field.  If you think it'll all be fine then that's great - go look at the evidence, reason critically and present your findings.  If you're not an expert and you're not prepared to present any evidence, please forgive me if I choose to believe the people who are qualified in the relevant field or are presenting compelling evidence.

Now, I have a problem with the original interpretation that the public voted for a "hard-brexit" for two reasons: Firstly, because the official Vote Leave campaign, and several other campaigns explicitly stated that voting "leave" would not lead to us leaving the single market, and as the referendum itself didn't specify one way or the other, there is absolutely no indication that this is what the majority actually wanted; and secondly because the voters only voted to leave by a fairly slim majority, so reasonably a government should also be working to accommodate the significant minority too. It seems to me that it would have been more reasonable for the government to try and accommodate both sides by taking some middle ground and heading for the "Norway solution", and indeed that is what most of the high profile campaigners on the "leave" side had been promoting immediately prior to the referendum.

Secondly, the government has put about more and more rhetoric that people voted to leave the EU at any cost, and that a no-deal brexit is "the will of the people".  Since there seems to be a problem demonstrating that the public voted for a "hard brexit", demonstrating that they voted for a no-deal seems even more of a problem.  I've heard it said that David Cameron is on video saying, prior to the referendum, that a no-deal is a possibility, but I've not seen this video, nor been able to find it.  Regardless, the official Vote Leave campaign, and most of the other big "leave" campaigns didn't discuss this possibility, so I don't think you can reasonably say that people expected it to happen as a result of voting to leave the EU.

My summary on this is: if the referendum result doesn't clearly show that people voted for a "hard-brexit" or "no-deal" brexit, rather than one of the many other types that were possible, you need to actually ask rather than just making it up.


Thirdly, the idea that Parliament preventing a no-deal is somehow undemocratic doesn't seem to make sense: MPs are elected to represent all of their constituents.  In order to do this they talk to constituents, etc. and if their constituents are overwhelmingly against no-deal they have an obligation to represent that view and vote against a no-deal..

Similarly, I've seen claims that Parliament doesn't represent "the will of the people" because most MPs are in favour of remaining.  I can't comment on whether a majority of MPs actually are in favour of remaining these days (I don't think there has been any indicative vote on that?) but you can't forget that they were democratically elected a year after the original referendum.  If a constituency still wanted to leave the EU, they wouldn't have elected an MP who supports remaining.  It seems only reasonable to assume that the current mix of MPs is reasonably representative of their constituencies, so if they are at odds with the referendum that seems to be all the more reason to suspect that the referendum result is no longer an accurate representation of "the will of the people".  Similarly, its important to remember that many of the current MPs weren't the ones who voted to start the process of us leaving, many weren't the ones who voted to have a referendum in the first place, and many were elected on a manifesto of not supporting brexit.

Finally, if you have a democratically elected Parliament, the idea that it is ok to use constitutional loopholes to prevent them from overturning a referendum that happened long before they were elected seems bonkers.  People change their minds, and you can't argue that a "remain" supporting MP who was elected in 2017 shouldn't be allowed to represent those views because their constituency voted "leave" in 2016 doesn't make sense - if the people of a "leave" voting constituency still wanted to leave, why would they have elected a "remain" supporting MP?

Democratically elected MPs who stand up for what they honestly believe their constituents now want, rather than what they wanted 3 years ago, are not "traitors" - please stop calling them that.  "Traitor" is not synonymous with "has s slightly different vision of brexit than me".

And last, but not least, comments like "the EU aren't trying to give us a good deal" make absolutely no sense at all. The EU's obligations are to negotiate in a way that benefits their members, they are not obliged to give us a good deal. If we're lucky, there will be overlap between things that benefit them and things that benefit us, and that's where a good deal for both sides comes from. But the EU isn't going to harm themselves in order to give us a good deal, and why should anyone expect them to?  At the moment, it appears that the EU has decided that agreeing to the UK's "red lines" is more harmful to them than a no-deal, and the only way we can change their opinion is by changing our red lines.

The fact that we are told that "give us a deal or we'll shoot ourselves in the head" is our best negotiating tactic really underlines just how weak the UK's negotiating position is, and if we won't compromise why would we expect the EU to?

Thursday, 29 August 2019

Carbon footprint

I've been doing some work estimating the carbon footprint of running servers with the aim to offset our products.  There are obviously two main parts to this: the emissions caused by manufacturing, supplying (and, at the end of its life, disposing of) the hardware, and those caused by actually running the hardware.  The former is a one-off cost each time you buy a new server, whereas the latter is the ongoing cost (e.g. electricity for powering the server, the air conditioning to keep it cool, etc.)

There are lots of different types of emissions that contribute to climate change, and for simplicity these are all summed together and expressed as kilograms of CO2 equivalent (kgCO2e).

Dell, helpfully, publish carbon footprint figures for their hardware, but unfortunately don't explain their methodology and some of the figures look suspiciously like a work of fiction to me.  I'll look at the Dell PowerEdge R440 as an example.

Dell's data sheet estimates a total carbon footprint and breaks down the carbon footprint into several aspects by percentage.  So I can use that total and the breakdown to calculate the carbon footprint of each aspect:
AspectPercentageEmissions
Manufacturing15.7%1155.52 kgCO2e
Transportation0.3%22.08 kgCO2e
Use83.9%6175.04 kgCO2e
EoL0.1%7.36 kgCO2e
TOTAL100%7360 kgCO2e

The data sheet estimates it uses 1480.002 KWh / year, and they assume a 4 year life, so that's 5920.008 KWh over its life.  They don't say what "Use" actually includes - I'm assuming that it is just the electrical power consumed by the server.

The amount of CO2e created in order to generate a KWh of electricity depends on how you're generating it - wind, hydro, solar, nuclear, etc. produce low emissions, coal produces very high emissions, gas is somewhere in the middle.  In the UK, DEFRA publish annual conversion factors based on the current generation mix on the national grid.  This changes year to year (the trend is downwards as we add more green capacity to the grid) and in 2019, this conversion factor is 0.2773 kgCO2e / KWh including transmission and distribution.  Other countries have a different mix of generating capacities, so will need a different conversion factor.

So, given the electricity consumption that Dell estimate over the server's life (5920.008 KWh), the emissions quoted for "Use" seem outrageously high - the conversion factor they seem to have used works out at 1.043 kgCO2e / KWh - almost 4 times the DEFRA figures.

A 2011 report from the Parliamentary Office of Science & Technology estimates that coal power (which is the worst case) produces 0.786-0.990 kgCO2e / KWh, so Dell's figure is even worse than the worst case of running the hardware off 100% coal power.

Its possible that their "Use" figure also includes the air conditioning required to keep the server cool.  If this is the case it makes their figures quite useless since they don't actually say that's what they're doing.  A rule of thumb is that about 50% of the power consumed by a data centre goes on air conditioning, so that would make their conversion factor 0.5215 kgCO2 / KWh - still way above DEFRA's figures for 2019.  In fact, even DEFRA's conversion factor from 2002 is significantly lower than this.

Unfortunately, very few other server vendors seem to publish figures to use as a comparison.  I couldn't find anything for HP kit (they provide a carbon footprint calculator, but this is only for printers, workstations and stuff rather than servers, and it also doesn't work at all).  Lenovo don't publish any information for their servers, but they do for workstations - although I haven't analysed their numbers in depth, they do look more reasonable than Dell's, attributing around 50% of the emissions to "use".

Recalculating Dell's figures using DEFRA's conversion factors, I would expect something like:
AspectPercentageEmissions
Manufacturing40.9%1155.52 kgCO2e
Transportation0.8%22.08 kgCO2e
Use58.1%1641.62 kgCO2e
EoL0.3%7.36 kgCO2e
TOTAL100%2826.58 kgCO2e

This looks more in line with Lenovo's figures.

Thursday, 28 March 2019

Netfilter's conntrack

People who use Linux for firewalling tend to use iptables to set up their rules.  The subsystem in the Linux kernel that actually does the firewalling is called Netfilter.

I've never found a complete description of all of Netfilter's features, especially some of the lesser used ones.  So here is a bit of an overview which includes a few recent discoveries that I've not seen documented elsewhere:

Netfilter includes a connection tracker, which can keep track of each flow that the system is handling.  Each flow has a 32 bit value called the connection mark (connmark), which you can use for anything you like.  This mark allows you to record 32 bits of information that persists as long as the flow does rather than having to treat each packet in complete isolation.

Packets traversing through the system are always in one of the following connection tracking states: UNTRACKED, INVALID, NEW, ESTABISHED, RELATED.

UNTRACKED and INVALID refer to packets that are either explicitly being excluded from connection tracking, or that the connection tracker doesn't think are valid for the current state of the flows that it knows about.

When a new flow is established, the first packet is in either the NEW or RELATED state, and subsequent packets are in the ESTABLISHED state.  RELATED means that netfilter thinks that the new flow is somehow related to another flow, and therefore shouldn't be handled in complete isolation.

I've seen information elsewhere that says that when a flow starts in the RELATED state, it inherits the connmark from the parent.  Experimentation shows that this isn't entirely accurate (or at least, not entirely clear).  It turns out that flows that start in the RELATED state permanently share the same connmark data with the flow(s) that they are related to.  This means that if any of the flows change their connmark, those changes also affect any other flows that they are related to.

The REJECT filter target asks the kernel to drop the packet being processed and reply with some kind of packet that indicates that it was rejected.  For example, "-j REJECT --reject-with tcp-reset" will respond with a TCP RST packet.  The response packet originates in the OUTPUT chains and has a state of RELATED, rather than being considered part of the original connection as you might expect.

In the case of rejecting connections with a TCP RST packet, the RST will, of course, have the same 5-tuple as the original TCP connection.  There doesn't appear to be any way of accessing a unique ID that identifies the flow, so as far as I can tell it is (probably) impossible for an external application to reliably tell the difference between packets belonging to the original (rejected) flow, and packets belonging to the related flow that carries the RST.

It is a shame that a flow ID isn't made available to user applications through the NFLOG / NFQUEUE interface.  Some poking around suggests that a flow ID *might* be available through the NFQA_CT section of the netlink message, so that warrants further investigation maybe.