buffrr / letsdane

🔒 Let's DANE is an experimental way to enable the use of DANE/TLSA in browsers and other apps using a lightweight proxy.
Apache License 2.0
111 stars 11 forks source link

Building Static Binaries for multi platform deployment. #9

Open Anunayj opened 3 years ago

Anunayj commented 3 years ago

Static Binaries for each platform can be built using:

Linux

Static Binaries for Linux can be made using the following Dockerfile (or running the commands in Dockerfile manually).

# DOCKER_BUILDKIT=1 docker build -f static.Dockerfile -o build/ . 
FROM golang:alpine AS base
RUN apk update && apk add linux-headers gcc make perl musl-dev expat-dev openssl-libs-static openssl-dev

#Install Unbound
FROM base as setup-unbound
WORKDIR /tmp
RUN wget https://www.nlnetlabs.nl/downloads/unbound/unbound-1.13.1.tar.gz && tar -xzf unbound-1.13.1.tar.gz
WORKDIR /tmp/unbound-1.13.1
RUN ./configure --prefix=/opt/install
RUN make && make install

FROM base as builder
#Install Dependencies
COPY --from=setup-unbound /opt/install/ /usr/local
#Optionally export this layer and cache this permanently somewhere.

#Copy stuff overgo mod download
WORKDIR /tmp/dane
COPY go.mod /tmp/dane/go.mod
RUN go mod download
#Will allow caching dependencies in layers.

COPY . /tmp/dane/
WORKDIR /tmp/dane/cmd/letsdane
#Build Static
RUN go build -tags unbound --ldflags '-extldflags "-lunbound -lssl -lcrypto -static"'

FROM scratch
COPY --from=builder /tmp/dane/cmd/letsdane/letsdane /
ENTRYPOINT [ "letsdane" ]

Use DOCKER_BUILDKIT=1 docker build -f static.Dockerfile -o build/ . to export the binary into build directory.

Running this first time can take >500 seconds (Mostly because of all the compiling that needs to be done for unbound and openssl), The Image layer can be exported to Dockerhub and used repeatedly if necessary. Subsequent builds should take <100 seconds.

Todo:

Anunayj commented 3 years ago

This Dockerfile can be used to automatically make builds using GitHub Actions, though I'm unsure how I should deal with the dependencies part, Should I export that as a Image and keep it on Dockerhub (and use FROM anunayj/go-alpine-unbound) or should I keep it like this and let the cache do it's job. (GitHub Actions evicts cache after 7 days of inactivity).

Anunayj commented 3 years ago

Also I have no idea what was causing the problems compiling earlier when i was using a lxd container. I guess I installed unbound from apk and then built it from source causing a unholy mess. I really feel like Alpine packagers should include static library (.a) for libraries in their -dev packages. Specially considering libmusl is used for static binaries, and their distribution is one of they few that implements libmusl.

Anunayj commented 3 years ago

Alternatively to save time/or if you are lazy to compile openssl this can be used. (Remember you will basically be trusting me to have shipped a exploit-free version of unbound library)

FROM anunayj/golang-libunbound@sha256:4db0797175be0d38f0a65f81517d1862a72c745cfb42d53b09aed477f00d6e5d as builder
#Install Dependendencies
WORKDIR /tmp/dane
COPY go.mod /tmp/dane/go.mod
RUN go mod download
#Will allow caching dependencies in layers.

COPY . /tmp/dane/
WORKDIR /tmp/dane/cmd/letsdane
#Build Static
RUN go build -tags unbound --ldflags '-extldflags "-lunbound -lssl -lcrypto -static"'

FROM scratch
COPY --from=builder /tmp/dane/cmd/letsdane/letsdane /
ENTRYPOINT [ "letsdane" ]

Sources for intermediate image here https://gist.github.com/Anunayj/f58463793ef3902eb4d0f4a24ce8b875

Anunayj commented 3 years ago

I have no idea how one would go about compiling for windows and mac, go does allow you to specify to build for windows or darwin, but how would that work with unbound? GitHub actions does provide both windows and macOS images to run stuff on which can be used to make binaries, but I don't have a MacOS machine to test stuff on, I'll try messing about on my windows machine later.

P.S. We really need a Windows subsystem for Linux haha.

Anunayj commented 3 years ago

Also are https://www.openssl.org/source/openssl-1.1.1j.tar.gz and https://www.nlnetlabs.nl/downloads/unbound/unbound-1.13.1.tar.gz updated with appropriate security fixes if there is one?

buffrr commented 3 years ago

Should I export that as a Image and keep it on Dockerhub (and use FROM anunayj/go-alpine-unbound) or should I keep it like this and let the cache do it's job. (GitHub Actions evicts cache after 7 days of inactivity).

Building static binaries isn't a frequent process if it's only done for every release. It's okay to leave it to build from source even if takes a bit of time. It's unfortunate that alpine doesn't include *.a libs. For openssl it seems there is a package on alpine openssl-libs-static which i found here https://pkgs.alpinelinux.org/package/edge/main/x86/openssl-libs-static. It could save time if we did apk add openssl-dev openssl-libs-static instead it of building openssl (i didn't test this)

I have no idea how one would go about compiling for windows and mac, go does allow you to specify to build for windows or darwin, but how would that work with unbound? GitHub actions does provide both windows and macOS images to run stuff on which can be used to make binaries, but I don't have a MacOS machine to test stuff on, I'll try messing about on my windows machine later.

Unbound is an optional dependency so letsdane can be built on most OSs with just go build (assuming the user has a validating resolver like hsd).

building on macOS with unbound is pretty simple (shared libs)

brew install unbound
git clone ... 
go build -tags unbound

To bundle unbound in the same binary, I built it this way (on MBP running Big Sur). I just installed unbound using brew (so it's built with some other dependencies that we probably don't need like nghttp2) but here's a quick way to build it:

macos prefers dynamic libraries so if there is a dynamic lib and a static lib in the same directory it will pick the dynamic one. So I specified the path for each .a lib

CGO_LDFLAGS="/usr/local/opt/unbound/lib/libunbound.a \
             /usr/local/opt/openssl/lib/libssl.a \
             /usr/local/opt/openssl/lib/libcrypto.a \
             /usr/local/opt/nghttp2/lib/libnghttp2.a \
             /usr/local/opt/libevent/lib/libevent.a" go build -tags unbound

it's not clear to me yet if that's the best way to do it.

Also are https://www.openssl.org/source/openssl-1.1.1j.tar.gz and https://www.nlnetlabs.nl/downloads/unbound/unbound-1.13.1.tar.gz updated with appropriate security fixes if there is one?

Those are links to the latest stable versions. They must be updated if new versions come up. Unbound includes a link for latest version https://nlnetlabs.nl/downloads/unbound/unbound-latest.tar.gz which is always updated (but this probably wouldn't be very stable if they include a new dependency or change something that affects compatibility)

Anunayj commented 3 years ago

Building static binaries isn't a frequent process if it's only done for every release. It's okay to leave it to build from source even if takes a bit of time. It's unfortunate that alpine doesn't include *.a libs. For openssl it seems there is a package on alpine openssl-libs-static which i found here https://pkgs.alpinelinux.org/package/edge/main/x86/openssl-libs-static. It could save time if we did apk add openssl-dev openssl-libs-static instead it of building openssl (i didn't test this)

Just tested it, and it works, updated the dockerfile.

Anunayj commented 3 years ago

I tried making binaries for macos using this command

C_INCLUDE_PATH="$(brew --prefix)/include" LIBRARY_PATH="$(brew --prefix)/lib" CGO_LDFLAGS="$(brew --prefix)/lib/libunbound.a \
  $(brew --prefix)/lib/libssl.a \
  $(brew --prefix)/lib/libcrypto.a \
  $(brew --prefix)/lib/libnghttp2.a \
  $(brew --prefix)/lib/libevent.a" go build -tags unbound ./cmd/letsdane

but seems like it still dynamically linked the binary?

linux-vdso.so.1 (0x00007fff935e3000)
    libunbound.so.8 => not found
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f5da032b000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f5da0139000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f5da035f000)

if I use

C_INCLUDE_PATH="$(brew --prefix)/include" CGO_LDFLAGS="$(brew --prefix)/lib/libunbound.a \
  $(brew --prefix)/lib/libssl.a \
  $(brew --prefix)/lib/libcrypto.a \
  $(brew --prefix)/lib/libnghttp2.a \
  $(brew --prefix)/lib/libevent.a" go build -tags unbound ./cmd/letsdane

I get

go: downloading github.com/buffrr/hsig0 v0.0.0-20200928223456-eca10c3b5481
go: downloading github.com/decred/dcrd/dcrec/secp256k1/v3 v3.0.0
go: downloading github.com/miekg/dns v1.1.31
go: downloading golang.org/x/net v0.0.0-20190923162816-aa69164e4478
go: downloading golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
go: downloading golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe
go: downloading github.com/miekg/unbound v0.0.0-20180419064740-e2b53b2dbcba
# github.com/miekg/unbound
/usr/bin/ld: cannot find -lunbound
collect2: error: ld returned 1 exit status
Error: Process completed with exit code 2.