Freeradius modules in Go

If you want to use radius sooner or later you will probably discover freeradius. It is a handy little radius AAA (authentication, authorization, accounting) server with a lot flexibility and a small enough footprint most embedded devices (router etc.) probably are able to run it.

freeradius also provides a lot of flexibility how to authorize your users provided by several modules. Currently you have three choices to create modules in freeradius, you can use rlm_python and use python or rlm_perl and use perl or write a new module from scratch in C. Anyhow, I thought only using python, perl or c is kinda boring and I wanted to see if I can use Go to write a freeradius module. Go allows to export libraries as shared c library after all.

First attempt

At first I though, well freeradius installs header files and libraries on my system, I can simply include the header files in the includes of my CFLAGS and link against the libraries via configuring LDFLAGS correctly. So I created my little proof of concept project in $GOPATH/src/github.com/dereulenspiegel/freeradius, created a little .go file and configured my CFLAGS and LDFLAGS

...
// # cgo CFLAGS: -I<path to freeradius headers>
// # cgo LDFLAGS: -lfreeradius-server....
...

I was naive and was soon greeted with:

fatal error: freeradius/autoconf.h No such file or directory

After a short search I found out that freeradius isn’t prepared for out of tree modules. A quick search on google revealed this helpful message:

Don't do that.

  The build system is designed to build modules in the source tree.  If you're doing something else, either:

a) build the module in the source tree

or

b) fix the build system to allow "out of tree" builds.

  Of the two options, (a) is the easiest.

  Alan DeKok.

Ok, fuck! The Go build tool is pretty dependent on everything being in my GOPATH and freeradius depends on having every module in its tree.

Go code where no Go code has gone before

I had the choice to either rewrite the whole build system of freeradius to allow out of tree modules or somehow teach my Go code to be build in the tree of freeradius and by the freeradius build system. Changing the build system of freeradius was out of question for me. So I had to teach my Go code to live and be build in the freeradius tree, which was actually not that complicated.

First we need to create a new folder under src/modules in the freeradius source tree. freeradius uses a variant of boilermake to build the server, tools and all the modules. And as far as I can see there is no way around that short of creating a new build system for freeradius. So we stick with boilermake. Actually boilermake seems to be pretty great if you only handle pure c code. But we don’t do that. Instead we need to trick boilermake into building our Go code and trick Go to think that it lives in a valid GOPATH. So we will create a file called src/modules/<our-cool-module>/all.mk describing our package name and module name.

PACKAGE  = rlm_golang
TARGET = $(PACKAGE).a

Tricking the GOPATH

What we are actally doing is create some kind of mini Go environment inside of the freeradius source tree. So we will create the minimal necessary directory structure for a GOPATH and link our module sources into the right location.

BASE_DIR = ${top_srcdir}/src/modules/$(PACKAGE)
GOPATH   = $(BASE_DIR)/.gopath
BASE     = $(GOPATH)/src/$(PACKAGE)

$(BASE):
  @mkdir -p $(dir $@)
  @ln -sf $(BASE_DIR) $@

As you can see we create our GOPATH inside of our module directory and link the module directory under the modules basename in the GOPATH src folder. The target $(BASE) will be used later on by other targtes. Next up we need to actually build our go code. So we create the following target:

go_build_dynamic: $(BASE)
  cd $(BASE) && CGO_CFLAGS='$(CGO_CFLAGS)' go build -buildmode=c-shared  -o $(top_builddir)/$(BUILD_DIR)/lib/local/$(PACKAGE).dylib ./

As you can see this target invokes $(BASE) first to ensure that the GOPATH is properly setup. Then it changes into our GOPATH and builds the code there as a shared c library.

In theory we can now invoke the target go_build_dynamic and build our code. Unfortunately the build system of freeradius doesn’t care about our target and never calls it.

Tricking boilermake

The build system in freeradius is not only responsible for building all its parts reliably, but also to provide the correct CFLAGS and LDFLAGS and to take some work off your hands. If you ever saw the Makefile of an average C project you are probably surprised that

TARGET    := rlm_exec.a
SOURCES   := rlm_exec.c

is the actual Makefile for the rlm_exec module in freeradius and does everything that is necessary to hook into the build system, build the object files and link them into a shared library. boilermake provides a lot of magic to make this happen. And every developer knows that such a level of magic is only great as long as you do intended things.

The magic of boilermake is basically adding templated targets to your mini Makefile which are then called during the build process. We can use this to hook into the build process. You will notice that we omitted the SOURCES variable in our Makefile. This is necessary to prevent boilermake from taking our C sources and building them. CGo will do this for us. To hook into the build process we need to find a target boilermake will normally call to compile our module. Unfortunately we can’t overwrite the commands in this target, but we can add a new dependency to the target. Sine we left the SOURCES variable undefined no C compiler will be called. After a lot of trying to remember the advanced parts of Makefiles I finally found the target build/lib/local/rlm_golang.la. We can use this target to ‘inject’ our target go_build_dynamic

build/lib/local/rlm_golang.la: go_build_dynamic

This line tells make that the target build/lib/local/rlm_golang.la has now the additional dependency go_build_dynamic and therefore make will call go_build_dynamic before build/lib/local/rlm_golang.la (which does nothing because we left SOURCES undefined).

When we call now make you will notice that boilermake actually tries to build our module and even calls the Go build tool. But the build will fail with

fatal error: 'freeradius-devel/radiusd.h' file not found

We still haven’t setup our CFLAGS correctly. Fortunately boilermake provides the necessary CFLAGS in the variable CFLAGS to our Makefile. Unfortunately we can’t use these flags directly. The includes are all relative to the top of the source tree, but our Go build doesn’t know about the freeradius source tree. This is now a dirty hack which can probably fail in future versions of freeradius. But what I did is, I printed out the CFLAGS variable and converted the relative include paths to absolute paths with the help of some provided make variables:

CGO_CFLAGS := -I. -I${top_srcdir}/src \
  -include ${top_srcdir}/src/freeradius-devel/autoconf.h \
  -include ${top_srcdir}/src/freeradius-devel/build.h \
  -include ${top_srcdir}/src/freeradius-devel/features.h \
  -include ${top_srcdir}/src/freeradius-devel/radpaths.h

This creates a variable containing the necessary CFLAGS for CGo. In our target go_build_dynamic you can already see that we are exporting this variable to the shell calling the Go build tool. Everything in the environment variable CGO_CFLAGS is added by CGo to its CFLAGS.

Now we can run make again, and expect the module to build. And indeed a valid shared module is created, but after that the build breaks again with

ar: no archive members specified

Basically we are done with building with the shared library, but our builds breaks which prevents other modules and parts of freeradius from building. The reason for this error is that the build system is now expecting a collection of compiled object files which it now wants to link together to a library. But CGo has already build the library for us and took care of all the linking etc. As I mentionend before it is not possible to overwrite commands of targets defined by boilermake since boilermake targets are defined after the targets in your Makefile. So there is no way of preventing boilermake from calling the linker step. But we can trick boilermake into doing nothing. boilermake allows us to define a different linker for every module. We can use this to replace the call to ar with a call to something which basically doesn’t to anything no matter the parameters. For now I settled simply with calling echo. It won’t do anything which prevents the build from succeeding and is available on every system. We can replace the linker with the line

TGT_LINKER := "echo"

The only downside to this is, that you will have a weird line with ar arguments printed in your build output. But now the build will finally run through and don’t block the build of other components.

What’s missing

Well all of this is more or less a dirty hack. I don’t expect that the freeradius project will accept any pull requests containing Go based modules with this setup. There are several paths and names more or less hardcoded (like the boilermake target we hook into, or the output path for the Go build tool) and we broke make in the way how it detects what to build, because the shared library is put in the wrong directory (probably an easy fix). So there is still a lot missing (like dynamically choosing the file extension for the shared lib) and I need to understand more of boilermake, the freeradius specific changes to boilermake and how it all connects to make this solution more elegant. And I need to actually implement a fully functioning freeradius module. Right now my test module simply loads and does nothing more useful, but it proves that the build works.

For reference here is the complete all.mk

.PHONY: go_clean go_build_dynamic

PACKAGE  = rlm_golang
BASE_DIR = ${top_srcdir}/src/modules/$(PACKAGE)
GOPATH   = $(BASE_DIR)/.gopath
BASE     = $(GOPATH)/src/$(PACKAGE)

TARGET = $(PACKAGE).a

# Very dirty hack, to prevent ar from trying to link stuff together. cgo has already taken care of that
TGT_LINKER := "echo"

# TODO somehow use regex to add ${top_srcdir} before every include path
CGO_CFLAGS := -I. -I${top_srcdir}/src \
  -include ${top_srcdir}/src/freeradius-devel/autoconf.h \
  -include ${top_srcdir}/src/freeradius-devel/build.h \
  -include ${top_srcdir}/src/freeradius-devel/features.h \
  -include ${top_srcdir}/src/freeradius-devel/radpaths.h

go_build_dynamic: $(BASE)
  cd $(BASE) && CGO_CFLAGS='$(CGO_CFLAGS)' go build -buildmode=c-shared  -o $(top_builddir)/$(BUILD_DIR)/lib/local/$(PACKAGE).dylib ./

$(BASE):
  @mkdir -p $(dir $@)
  @ln -sf $(BASE_DIR) $@

build/lib/local/rlm_golang.la: go_build_dynamic

clean_rlm_golang.la: go_clean

go_clean:
  @rm -rf $(GOPATH)

I hope this us useful for some and that we see some cool Go based freeradius modules in the future.

Tue Jul 18, 2017