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.