fluxy: Putting My Idle IP Address Pool to Work
Sunday, November 17, 2024 / , , , ,
AI TranslationSimplified ChineseEnglish
Summary:
I have a /24 of IPv4 space and a /44 of IPv6, but most of the addresses sit idle. So I wrote fluxy, an HTTP proxy that randomly picks a source address from the pool for every connection.
Ever since I got my own ASN, I've had a /44 of IPv6 space, and later I added a /24 of IPv4. I announce these prefixes through BIRD, so they're reachable from across the Internet. But in practice, I only use the few addresses configured on my servers. Most of the rest sit idle.
A /24 contains 256 IPv4 addresses, while a /44 contains
2^{84}IPv6 addresses—enough that I can pretty much treat the supply as unlimited. And sometimes I do need an address pool so that different requests can leave from different addresses. So I wrote fluxy, an HTTP proxy that randomly picks a source address from my pool for every connection that passes through it.Prerequisites
fluxy works by binding a socket to an address from the pool before opening a connection.
First, return traffic has to be able to reach those addresses. In other words, I need to announce the prefixes through BIRD so that replies to packets sent from them can find their way back. I've covered the BIRD configuration before: add the prefixes under protocol static, then export them over the BGP session.
I also need the machine to accept replies sent to these addresses and allow sockets to bind to them. But I can't configure hundreds of addresses—or even
2^{84}of them—on an interface one by one. First, I add two routes of type local through lo so that the kernel delivers packets addressed to the entire prefixes to the local machine.These two routes handle incoming replies, but IPv4 and IPv6 differ slightly in their bind checks. For IPv4, a socket can bind directly to an address covered by the local route. For IPv6, I also need to allow binding to addresses that haven't been individually configured on an interface.
This setting only applies until the next reboot. To make it persistent, I can put net.ipv6.ip_nonlocal_bind=1 in a configuration file under /etc/sysctl.d/.
For IPv4, ip addr add 44.31.223.0/24 dev lo also creates a local route for the entire prefix. This is special handling for the loopback interface. If I want to put the addresses on a dummy interface instead, I still need to explicitly configure a local route for the whole prefix; ip addr add alone isn't enough.
That completes the path.
Outgoing packets still follow the default route through my upstream; only the source address changes to one from the pool. On the way back, because I'm announcing the prefix, replies follow the BGP route to this machine, where the local route delivers them to the corresponding socket.
Picking a Random Address
The idea is simple: keep the network bits of the prefix, generate random host bits, and combine the two with a bitwise OR. An IPv6 address fits in a u128, so I can do the whole thing with integer operations.
Shifting right and then left clears the host bits of the prefix, leaving the network portion. For the random number, I do the reverse: shift left and then right to clear the high bits and keep only the host bits. Combining the two gives me a random address within the prefix. A /44 leaves 84 host bits, so the chance of picking the same address twice is tiny; in practice, each connection gets its own unique source address. The IPv4 version works the same way, just with a u32.
The HTTP Proxy
The proxy uses hyper on top of tokio. HTTP proxy requests come in two forms, so there are two ways to handle them.
The first is CONNECT. When accessing an HTTPS site, the client first sends a CONNECT request asking the proxy to connect to the destination's port 443. Once connected, the proxy just shuttles data between the two ends. So when fluxy receives a CONNECT request, it creates a socket, binds it to a random address, connects to the destination, and then forwards traffic in both directions.
The other form is a regular HTTP request: the proxy makes the request on the client's behalf, then passes the response back unchanged. Here I use hyper's built-in client. Its HttpConnector lets me specify a local address, so I create a new connector for each request and set it to a random address.
Each connection prints the address it used, in a line like example.com:443 via [2a0e:aa07:e0a0:...]:..., so I can check that the addresses are being selected and rotated.
Installing and Running It
fluxy is published on crates.io and can be installed with cargo.
When starting it, specify the listen address and the two address pools.
-b sets the listen address; if omitted, it defaults to 0.0.0.0:51080. -4 and -6 specify the IPv4 and IPv6 pools. Both are optional, so configuring just one is enough. I don't have to use an entire prefix either. In this example, I give fluxy just one /48 within my /44, so it only picks addresses from that /48.
Then I can use curl to access an IP lookup service through the proxy. Repeating the request a few times shows a different address each time.