OpenWrt Forum Archive

Topic: OpenVPN with netifd

The content of this topic has been archived on 5 May 2018. There are no obvious gaps in this topic, but there may still be some posts missing at the end.

Hello all,

currently OpenVPN is used as a service, but in my opinion it should be more treated as a virtual network device much like pppoe.
That's why I'm creating a netifd/proto script that takes care of starting the openvpn instance on ifup (and kills it on ifdown).
This way, the tunnels are created device-specific during network initialization and a broken tunnel is automatically restarted.

Advantages of the network-interface approach:
- up/down of each tunnel handled independently by network init
- tunnel configuration as (virtual) network interface
- easier integration in firewall zones, routing, etc.
- automatic reconnect on failure/network restart

Now here's my question:
Do most of you think this is a good way to go or are there any disadvantages in using openvpn the same way as pppoe?

Cheers
Joachim

Yes, integrating OpenVPN as interface protocol is a good idea and we'd certainly accept patches doing this.
However it needs some sane strategy - I do not think it would be a good idea to stuff all possible OpenVPN options into the network config, instead maybe something like "config interface foo; option proto openvpn; option instance xyz" where "xyz" would either refer to a config file, e.g. /etc/openvpn/xyz.conf or a section in /etc/config/openvpn.

One problem I see is to determine the vpn ifname in advance - unless the integration forcibly passes options like "dev-type tun" and "dev ovpn-xyz" it can get very hard to predict the device name in advance without parsing the config. Even then it might be unclear because the to-be-created tuntap iface is automatically allocated.

We could map the uci "ifname" option to --dev and --dev-type when launching the instance, where the type is maybe auto-determined based on the name (e.g. "tun0" or "tun-work" -> "tun" / "tap11" or "tap-office" -> "tap").

There also needs to be some way to suppress harmful options from external configs, like for example "redirect-gateway" - that should probably get mapped to the existing "defaultroute" uci bool.

That perfectly matches with what I've already scripted. I also prefer the openvpn config file much more than stuffing openvpn option into uci, so up to now I've only implemented 3 options: 'config', 'dev' and 'dev_type' (whereas the latter two are optional).

So my network config looks like:

config interface foo
  option proto 'openvpn'
  option config 'myserverconfig'    (mandatory, if the value contains a '/' it is used as config file with full path, if not then it is searched for '/etc/openvpn/${value}.conf')
  option dev 'vpn0'      (optional: can also be specified in the config file)
  option dev_type 'tun'       (optional: can also be specified in the config file)

After a bit of cleanup and testing (probably today), I'm going to post my files here for further discussion.

I don't see yet why it is important to know the vpn ifname in advance, everything I've done so far doesn't require that. The only issue I've stumbled upon was the LuCI integration, where a little patch on /usr/lib/lua/luci/model/network.lua helped:

diff -Naur old/network.lua new/network.lua
--- old/network.lua     2012-09-12 07:45:25.495021315 +0200
+++ new/network.lua     2012-09-10 16:02:31.048423055 +0200
@@ -862,8 +862,11 @@

 function protocol.get_interface(self)
        if self:is_virtual() then
-               _tunnel[self:proto() .. "-" .. self.sid] = true
-               return interface(self:proto() .. "-" .. self.sid, self)
+               _tunnel[self:ifname()] = true
+               return interface(self:ifname(), self)
        elseif self:is_bridge() then
                _bridge["br-" .. self.sid] = true
                return interface("br-" .. self.sid, self)

Every (virtual) network proto instance I've seen so far returns self:proto() .. "-" .. self.sid when self:ifname() is called, so that patch shouldn't change any existing behaviour. But it allows the openvpn proto instance to respond with the real name of the interface when the tunnel is up and return a default name when it's down.

Here's my /usr/lib/lua/luci/model/network/proto_openvpn.lua file so far:

local netmod = luci.model.network

local proto = netmod:register_protocol("openvpn")

function proto.get_i18n(self)
        return luci.i18n.translate("OpenVPN")
end

function proto.ifname(self)
        ifname = self:_ubus("l3_device")
        if ifname then
                return ifname
        else
                return "vpn-" .. self.sid
        end
end

function proto.opkg_package(self)
        return "openvpn"
end

function proto.is_installed(self)
        return nixio.fs.access("/usr/sbin/openvpn")
end

function proto.is_floating(self)
        return true
end

function proto.is_virtual(self)
        return true
end

function proto.get_interfaces(self)
        return nil
end
        
function proto.contains_interface(self, ifc)
        return (netmod:ifnameof(ifc) == self:ifname())
end
        
netmod:register_pattern_virtual("^vpn%d")
netmod:register_pattern_virtual("^vpn-%w")

Your point with suppressing harmful options is very good, I didn't think of that. My current implementation calls openvpn with the --route-noexec parameter because telling the routes to netifd in the up script additionally sets the routes which resulted in each route existing twice. So I'm telling openvpn to not create any routes and totally leave that to the scripts. This might even simplify detecting and filtering hostile pushed settings. I'm not even sure what the effect of a pushed 'redirect-gateway' would be when openvpn is called with --route-noexec, will test that.

Looks good so far but I'd really map "option dev" to "option ifname" to keep the style of the other protocols. I'd also call "option dev_type" something like "option mode routed" or "option mode bridged" instead.

Ok, done that (almost). Renamed "option dev" to "option ifname", "option dev_type" to "option type" (=routed|bridged) and added new "option mode" (=client|server).
The latter (option mode) is required because if the configuration file would create a openvpn server, it might be desirable to call client-connect and client-disconnect scripts.
If "option mode" is not specified or is set to "client", the openvpn instance is started without --client-connect and --client-disconnect.

I've also tested what happens when a "redirect-gateway" directive is pushed from the server. Good thing (in my opinion) is: nothing happens at all. Since openvpn is started with --route-noexec and the route creation is handled by the up script(s), openvpn doesn't try to create a new default gateway route and the up script won't either.
Now one can discuss if pushing a redirect-gateway directive from the server to the client must be supported - I doubt so. If one can setup the openvpn interface on the client to use the remote server as new default gateway as soon as the connection is established, that's probably enough. Why should the server decide?

Just to clarify it again: pushing routes still works, just pushing "redirect-gateway" (and pushing "route-delay", to be honest) doesn't have effect.

One final minor nitpick, the interfaces should be prefixed with "ovpn" instead of "vpn" because we might end up supporting other things like L2TP etc.
I could also imagine making the "ifname" option optional, in which case the resulting --dev would be "ovpn-$cfg"

Here's what I have working so far: http://pariah-angels.de/openwrt/openvpn-interface.tgz

This is what's not yet implemented:
- ipv6 routes not yet implemented, have not much experience with that yet
- option for 'redirect-gateway' function on the client
- ...and probably lots of things I didn't think about

I didn' read your post before, so the name scheme in the tgz still uses 'vpn' instead of 'ovpn' but that shouldn't change any behaviour.

The openvpn lua script requires the patched network.lua, patch is included in the tgz.

Thanks for having a look at it ;-)

I have worked with it for quite some time now on different platforms and it seems very stable and reliable to me, so I guess it's time to contribute it.

The package contains lots of scripts and nothing to compile, so what would be the best approach to build a new package?
1. have all scripts "inside" of the package residing in svn
or 2. make svn just contain a single Makefile which downloads the scripts (in a .tar.gz file) from some site that I host?

Approach 1 seems quite difficult for me to commit it - I would have to post all the script files to the openwrt-devel list, no?
But it would have the advantage that everybody could contribute/modify/patch the scripts - I don't claim my scripts to be perfect.

Approach 2 would be a lot easier for me to commit and develop - I could just create new packages on my server and have them downloaded.
Disadvantage would be that the community has to rely on me and my hosted server.

Can anyone give me an advice which way to go?

The discussion might have continued from here.