Route Squid Proxy Traffic Through an OpenVPN Gateway
When using a VPN, the default configuration is to route all traffic over the VPN tunnel. However, there are some instances where it is useful to only route application-specific traffic instead of forcing all traffic through the VPN. This isn't always possible, but if application in question supports an HTTP proxy then we can use Squid to transparently pass that traffic off to an OpenVPN gateway.
Permanently add a custom routing table for OpenVPN traffic:
echo "100 openvpn" >> /etc/iproute2/rt_tables
The addition/deletion of the actual routes will be added by the OpenVPN up/down scripts.
Traffic which is accepted by Squid must be emitted on the OpenVPN tunnel interface in order for this to work. Since IP address for the interface is dynamic (assuming the server is not configured to provide static IP addresses) this would require updating the Squid config file every time the VPN connection is established (AFAIK, a hostname cannot be provided in place of the IP address). To get around this limitation, we will emit traffic on a random IP address and then use iptables to forward traffic to the real IP address.
The following lines were added to the /etc/squid3/squid.conf config file:
acl local_net src 192.168.0.0/255.255.255.0 http_access allow local_net forwarded_for delete tcp_outgoing_address 10.202.101.1
The first line defines my local network, the second line instructs Squid to accept connections from any computer on my local network, and the third line prevents Squid from including the X-Forwarded-For header in its outgoing connections. The last line defines the source IP address for which Squid will use to make requests on behalf of users. Note that this IP address should not belong to your local subnet or the OpenVPN subnet.
OpenVPN Client Configuration
Add the following items to the client config file:
route-noexec route-up route-up.sh down down.sh
The route-noexec parameter prevents the client from applying the default routing configuration which is pushed from the server. The route-up parameter must point to a script which will be used to setup the custom routing and the down parameter must point to a script that will remove the custom routing once the VPN client is stopped.
The content of route-up.sh should look like this:
#!/bin/bash source "`dirname $0`/vpn_config.sh" logger "OpenVPN route-up.sh: bridge if: $VPNBRIDGE_IF addr: $VPNBRIDGE_ADDR table: $VPNBRIDGE_TBL" # Add the default route to our custom 'openvpn' routing table ip route add default via $ifconfig_remote dev $dev table $VPNBRIDGE_TBL # Add new interface to act as a middleman between the fixed address used # by Squid and the dynamic address provided by OpenVPN ip tuntap add dev $VPNBRIDGE_IF mode tun ip addr add $VPNBRIDGE_ADDR dev $VPNBRIDGE_IF ip link set dev $VPNBRIDGE_IF up # All packets from Squid are passed to the custom routing table ip rule add from $VPNBRIDGE_ADDR lookup $VPNBRIDGE_TBL # Rewrite the source address to match the one provided by OpenVPN iptables -t nat -A POSTROUTING -s $VPNBRIDGE_ADDR -j SNAT --to-source $ifconfig_local logger "OpenVPN route-up.sh: local: $ifconfig_local remote: $ifconfig_remote device: $dev"
The content of down.sh should look like this:
#!/bin/bash source "`dirname $0`/vpn_config.sh" logger "OpenVPN down.sh: bridge if: $VPNBRIDGE_IF addr: $VPNBRIDGE_ADDR table: $VPNBRIDGE_TBL openvpn local: $ifconfig_local" ip rule del from $VPNBRIDGE_ADDR lookup $VPNBRIDGE_TBL iptables -t nat -D POSTROUTING -s $VPNBRIDGE_ADDR -j SNAT --to-source $ifconfig_local ip tuntap del dev $VPNBRIDGE_IF mode tun
Finally, the common configurable parameters for both of these scripts is found in another script named vpn_config.sh. Its contents should look like this:
#!/bin/bash export VPNBRIDGE_IF=vpnbridge0 export VPNBRIDGE_ADDR=10.202.101.1 export VPNBRIDGE_TBL=openvpn
These three scripts should be placed in the same directory and must be marked as executable (chmod +x).
Theory of Operation
This solution is probably overcomplicated, but this is the best I could do with the limited networking skills I have. That being said, I'll attempt to explain the method here.
The route-up.sh script creates a new virtual tunnel network interface (named vpnbridge0) with the IP address which was provided for the tcp_outgoing_address parameter in the Squid configuration file (10.202.101.1). Additionally, the script instructs the kernel to route any traffic from that address to the custom 'openvpn' routing table. So when traffic is emitted from the proxy, it gets passed to the openvpn routing table but since the source IP address doesn't match the dynamic address provided by the VPN server, the request fails. To get around this, iptables NAT feature is used to rewrite the source address of the traffic to match that of the VPN client's local IP address. This makes the VPN software happy, and the traffic can flow!
As far as I can tell, this is how the traffic ends up flowing:
- A client sends a request to Squid.
- Squid binds to 10.202.101.1 and emits the request.
- The kernel's routing chain prepares to send the traffic to the route defined in the custom 'openvpn' routing table.
- The iptables POSTROUTING chain notices the packet has a source address of 10.202.101.1, so it rewrites it to match the $ifconfig_local address.
- The traffic is put onto the VPN's virtual network interface and VPN server handles the rest.