Sunday, December 26, 2010

CUPS, Host headers, NAT'd access and workarounds


A friend of mine has a Windows XP install which runs in a VirtualBox environment hosted by his openSUSE install. It works well for the two or three times a year it gets use. Recently, he needed to have the ability to print from the virtualized environment to his home printer. Since the printer was already set up on the host in CUPS, I thought that this would be a pretty trivial change to make. I was wrong.
The virtualized environment is using NAT for network access. This works really well. The environment is set up with an 'internal' DHCP server, using 10.0.2.0/24 by default (I might be wrong on the mask, but it's not relevant here). The 'host' gets 10.0.2.2 and the guest gets one of the other addresses. This way, the guest can access resources on the host using the 10.0.2.2 address, and whatever the host has access to, the guest does too (I'm generalizing and hand-waving a bit here). Since this is really NAT'd, services on the host 'see' access from localhost, and most services work great.
What I decided to start with was to access http://10.0.2.2:631/ from the guest to see if I could talk to the CUPS server on the host. If I could do that, setting up a print driver from there should be trivial. No such luck. Checking the logs in CUPS is usually an exercise in futility, but it's good practice anyway. There wasn't much there except:
E [24/Dec/2010:07:59:01 -0600] Request from "localhost" using invalid Host: field "10.0.2.2:631"
Since CUPS is HTTP-based, what is happening is this: the client makes an HTTP request and supplies a Host header with the value "10.0.2.2:631", which is expected. The request (due to NAT) arrives over localhost and CUPS grumps because the arrival interface (localhost) and the Host header (10.0.2.2) don't match. Most HTTP servers allow you to configure aliases for which the server will still respond, and a quick google seems to suggest exactly that for CUPS. From what I read, it appeared that the problem can be solved by telling CUPS that 10.0.2.2 is OK, with either "ServerAlias 10.0.2.2" or "ServerAlias 10.0.2.2:631" or even "ServerAlias *" (accept everything). However, none of these, together or in combination, work. More googling seems to suggest that others have run into *similar* issues. See this and this and this and this.
None of those exactly addressed my issue, and the workarounds didn't work or were suboptimal for this situation (changing ServerName, for example).
I suppose I could have kept digging, but it seemed to me that the code was hard-wired to perform certain tests if the request came in over localhost, and I really didn't care to dig into it too much further.
So, what to do? I didn't want to change the networking mode of the virtualized environment -- for this usage scenario NAT is exactly correct, and any other mode has drawbacks. Since CUPS uses HTTP, and the problem is that the Host header isn't matching what CUPS expects it to match, I'll just fix that problem by writing a bit of code which accepts HTTP requests, strips the Host (and Connection, for good measure) headers, and relays that request to localhost:631. Python makes this trivial, and 92 lines of code later and I have a little CUPS proxy. It's dumb, it's got hard-coded bits in it, and it's not very tolerant, but it works and it works well. It listens on localhost:6310 and operates exactly as previously described. I become root, and fire up python to give it a try. Then I try to set up a printer in the virtualized environment, talking to http://10.0.2.2:6310/, and it went perfectly. Problem solved.
I put a bit more polish on the software, whip up a spec file, an init script, and even a logrotate config file and a few short seconds after 'rpmbuild -ba' I have an easily-installable RPM that I can install, manage the startup/shutdown of easily, and (most importantly), it solves an obnoxious problem.
Addendum: a quick view of the source confirms my suspicions. See scheduler/client.c, in the 'valid_host' function. If the request arrives over the localhost interface, and the Hoste header is present, it *must* match "localhost" or one of several other localhost equivalents.

2 comments:

Erik Jensen said...

I'm running into the same problem. Is your code available anywhere?

Jon said...

I'll try to clean it up and make it available. It's pretty trivial, which means I probably got bits of it wrong, but it does appear to work.