Compare commits

...
18 Commits
Author SHA1 Message Date
Joona HoikkalaandGitHub cd3a587e43 Release v0.4 (#53) 2018-03-15 00:47:36 +02:00
Joona HoikkalaandGitHub 439da9c09f Properly parse r.RemoteAddr (#50)
* Properly parse r.RemoteAddr

* Add tests, and fix net.ParseCIDR issues with IPv6 addresses enclosed in brackets
2018-03-15 00:23:55 +02:00
Joona HoikkalaandGitHub 5c2e60a828 Add configuration option to disable registration endpoint (#51) 2018-03-14 23:35:39 +02:00
Joona HoikkalaandGitHub d542ee03b5 Merge pull request #42 from cpu/cpu-differentiate-errors
API: Differentiate bad TXT update error.
2018-03-13 11:32:40 +02:00
Joona HoikkalaandGitHub 92f8cc2802 Merge branch 'master' into cpu-differentiate-errors 2018-03-13 10:55:02 +02:00
Joona HoikkalaandGitHub 978ac5d62b Merge pull request #49 from Yannik/patch-1
return cname if requested RR was not found and cname exists (fixes #48)
2018-03-13 10:42:44 +02:00
Yannik Sembritzki 4d214d7f52 return cname if requested RR was not found and cname exists 2018-03-03 11:24:32 +01:00
Daniel McCarneyandGitHub f463d07d0b Merge branch 'master' into cpu-differentiate-errors 2018-03-01 09:54:41 -05:00
Joona HoikkalaandGitHub 9a908d7d6b Log IP address that we're matching against allowFrom values stored in the DB (#46)
* Add logging for IP matching

* Fix typo
2018-03-01 16:53:38 +02:00
Daniel McCarneyandJoona Hoikkala 11c852ee91 README: simplify installation instructions with go get. (#41)
* README: simplify installation instructions with `go get`.

Prior to this commit the installation instructions in the README had you
use `git clone` and then `go build`. This can be collapsed into one step
using a more idiomatic `go get` command. This commit updates the README
accordingly.

* Also mention supported config file locations

* Fix go get URL
2018-02-28 20:38:44 +02:00
Joona HoikkalaandGitHub 32608e9f47 Merge pull request #44 from cpu/cpu-add-api-ip-example-config
README: Add example 'ip' in example API config.
2018-02-28 20:24:08 +02:00
Daniel 77b5fda6fb README: Add example 'ip' in example API config.
`acme-dns` supports binding the API to a specific interface instead of
all interfaces by providing an `ip` address in the `[api]` configuration
section. Prior to this commit the `ip` field wasn't shown in the example
configuration in the README. This commit adds an example showing how to
configure `acme-dns` to listen only on `127.0.0.1` to the example config
and describes what the default value (`""`) does.
2018-02-27 21:45:13 -05:00
Daniel efdd560ee4 API: Differentiate bad TXT update error.
Previous to this commit, if the update message had a valid subdomain but
an invalid TXT the error returned was for a bad subdomain. This can
confuse developers who were POSTing junk TXT records to test acme-dns
:-)

This commit adjusts the `webUpdatePost` error handling such that
`!validSubdomain(input)` and `!validTXT(input)` give distinct errors.

The `!validSubdomain` case should never happen in `webUpdatePost`
because `auth.go`'s `Auth` function already vets the post data
subdomain but I retained the error handling code just in case.

Unit tests for an update with an invalid subdomain and an update with an
invalid TXT are included.
2018-02-27 19:26:15 -05:00
Joona HoikkalaandGitHub 830cceb62c Release v0.3.2 (#40) 2018-02-01 16:10:24 +02:00
Leon KyneurandJoona Hoikkala 9f896759f0 reinstall ca-certificates (#39) 2018-02-01 16:02:06 +02:00
Joona HoikkalaandGitHub 476f6fc555 Only autobuild for linux, as sqlite needs cgo (#38) 2018-02-01 15:33:57 +02:00
Joona HoikkalaandGitHub e2e84ede6b Goreleaser config (#37) 2018-02-01 14:20:40 +02:00
Joona HoikkalaandGitHub 562d7cbad4 Make autocert use HTTP-01 challenge instead of TLS-SNI (#36) 2018-02-01 10:53:34 +02:00
13 changed files with 180 additions and 25 deletions
+17
View File
@@ -0,0 +1,17 @@
builds:
- binary: acme-dns
env:
- CGO_ENABLED=1
goos:
- linux
goarch:
- 386
- amd64
archive:
format: tar.gz
files:
- LICENSE
- README.md
- Dockerfile
- config.cfg
+1
View File
@@ -14,6 +14,7 @@ COPY --from=builder /go/src/github.com/joohoi/acme-dns .
RUN mkdir -p /etc/acme-dns
RUN mkdir -p /var/lib/acme-dns
RUN rm -rf ./config.cfg
RUN apk --no-cache add ca-certificates && update-ca-certificates
VOLUME ["/etc/acme-dns", "/var/lib/acme-dns"]
ENTRYPOINT ["./acme-dns"]
+13 -5
View File
@@ -110,13 +110,11 @@ Check out how in the INSTALL section.
1) Install [Go 1.9 or newer](https://golang.org/doc/install)
2) Clone this repo: `git clone https://github.com/joohoi/acme-dns $GOPATH/src/acme-dns`
2) Install acme-dns: `go get github.com/joohoi/acme-dns/...`
3) Build ACME-DNS: `go build`
3) Edit config.cfg to suit your needs (see [configuration](#configuration)). `acme-dns` will read the configuration file from `/etc/acme-dns/config.cfg` or `./config.cfg`
4) Edit config.cfg to suit your needs (see [configuration](#configuration))
5) Run acme-dns. Please note that acme-dns needs to open a privileged port (53, domain), so it needs to be run with elevated privileges.
4) Run acme-dns. Please note that acme-dns needs to open a privileged port (53, domain), so it needs to be run with elevated privileges.
## Using Docker
@@ -186,6 +184,12 @@ connection = "acme-dns.db"
[api]
# domain name to listen requests for, mandatory if using tls = "letsencrypt"
api_domain = ""
# disable registration endpoint
disable_registration = false
# autocert HTTP port, eg. 80 for answering Let's Encrypt HTTP-01 challenges. Mandatory if using tls = "letsencrypt".
autocert_port = "80"
# listen ip, default "" listens on all interfaces/addresses
ip = "127.0.0.1"
# listen port, eg. 443 for default HTTPS
port = "8080"
# possible values: "letsencrypt", "cert", "none"
@@ -214,6 +218,10 @@ header_name = "X-Forwarded-For"
```
## Changelog
- v0.4 Clear error messages for bad TXT record content, proper handling of static CNAME records, fixed IP address parsing from the request, added option to disable registration endpoint in the configuration.
- v0.3.2 Dockerfile was fixed for users using autocert feature
- v0.3.1 Added goreleaser for distributing binary builds of the releases
- v0.3 Changed autocert to use HTTP-01 challenges, as TLS-SNI is disabled by Let's Encrypt
- v0.2 Now powered by httprouter, support wildcard certificates, Docker images
- v0.1 Initial release
+4 -2
View File
@@ -5,6 +5,7 @@ import (
"net"
"github.com/satori/go.uuid"
log "github.com/sirupsen/logrus"
)
// ACMETxt is the default structure for the user controlled record
@@ -32,9 +33,9 @@ func (c *cidrslice) JSON() string {
func (c *cidrslice) ValidEntries() []string {
valid := []string{}
for _, v := range *c {
_, _, err := net.ParseCIDR(v)
_, _, err := net.ParseCIDR(sanitizeIPv6addr(v))
if err == nil {
valid = append(valid, v)
valid = append(valid, sanitizeIPv6addr(v))
}
}
return valid
@@ -47,6 +48,7 @@ func (a ACMETxt) allowedFrom(ip string) bool {
if len(a.AllowFrom.ValidEntries()) == 0 {
return true
}
log.WithFields(log.Fields{"ip": remoteIP}).Debug("Checking if update is permitted from IP")
for _, v := range a.AllowFrom.ValidEntries() {
_, vnet, _ := net.ParseCIDR(v)
if vnet.Contains(remoteIP) {
+12 -5
View File
@@ -66,7 +66,18 @@ func webUpdatePost(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
if !ok {
log.WithFields(log.Fields{"error": "context"}).Error("Context error")
}
if validSubdomain(a.Subdomain) && validTXT(a.Value) {
// NOTE: An invalid subdomain should not happen - the auth handler should
// reject POSTs with an invalid subdomain before this handler. Reject any
// invalid subdomains anyway as a matter of caution.
if !validSubdomain(a.Subdomain) {
log.WithFields(log.Fields{"error": "subdomain", "subdomain": a.Subdomain, "txt": a.Value}).Debug("Bad update data")
updStatus = http.StatusBadRequest
upd = jsonError("bad_subdomain")
} else if !validTXT(a.Value) {
log.WithFields(log.Fields{"error": "txt", "subdomain": a.Subdomain, "txt": a.Value}).Debug("Bad update data")
updStatus = http.StatusBadRequest
upd = jsonError("bad_txt")
} else if validSubdomain(a.Subdomain) && validTXT(a.Value) {
err := DB.Update(a)
if err != nil {
log.WithFields(log.Fields{"error": err.Error()}).Debug("Error while trying to update record")
@@ -77,10 +88,6 @@ func webUpdatePost(w http.ResponseWriter, r *http.Request, _ httprouter.Params)
updStatus = http.StatusOK
upd = []byte("{\"txt\": \"" + a.Value + "\"}")
}
} else {
log.WithFields(log.Fields{"error": "subdomain", "subdomain": a.Subdomain, "txt": a.Value}).Debug("Bad update data")
updStatus = http.StatusBadRequest
upd = jsonError("bad_subdomain")
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(updStatus)
+60
View File
@@ -159,6 +159,66 @@ func TestApiRegisterWithMockDB(t *testing.T) {
DB.SetBackend(oldDb)
}
func TestApiUpdateWithInvalidSubdomain(t *testing.T) {
validTxtData := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
updateJSON := map[string]interface{}{
"subdomain": "",
"txt": ""}
router := setupRouter(false, false)
server := httptest.NewServer(router)
defer server.Close()
e := getExpect(t, server)
newUser, err := DB.Register(cidrslice{})
if err != nil {
t.Errorf("Could not create new user, got error [%v]", err)
}
// Invalid subdomain data
updateJSON["subdomain"] = "example.com"
updateJSON["txt"] = validTxtData
e.POST("/update").
WithJSON(updateJSON).
WithHeader("X-Api-User", newUser.Username.String()).
WithHeader("X-Api-Key", newUser.Password).
Expect().
Status(http.StatusUnauthorized).
JSON().Object().
ContainsKey("error").
NotContainsKey("txt").
ValueEqual("error", "forbidden")
}
func TestApiUpdateWithInvalidTxt(t *testing.T) {
invalidTXTData := "idk m8 bbl lmao"
updateJSON := map[string]interface{}{
"subdomain": "",
"txt": ""}
router := setupRouter(false, false)
server := httptest.NewServer(router)
defer server.Close()
e := getExpect(t, server)
newUser, err := DB.Register(cidrslice{})
if err != nil {
t.Errorf("Could not create new user, got error [%v]", err)
}
updateJSON["subdomain"] = newUser.Subdomain
// Invalid txt data
updateJSON["txt"] = invalidTXTData
e.POST("/update").
WithJSON(updateJSON).
WithHeader("X-Api-User", newUser.Username.String()).
WithHeader("X-Api-Key", newUser.Password).
Expect().
Status(http.StatusBadRequest).
JSON().Object().
ContainsKey("error").
NotContainsKey("txt").
ValueEqual("error", "bad_txt")
}
func TestApiUpdateWithoutCredentials(t *testing.T) {
router := setupRouter(false, false)
server := httptest.NewServer(router)
+7 -1
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"github.com/julienschmidt/httprouter"
@@ -83,5 +84,10 @@ func updateAllowedFromIP(r *http.Request, user ACMETxt) bool {
ips := getIPListFromHeader(r.Header.Get(Config.API.HeaderName))
return user.allowedFromList(ips)
}
return user.allowedFrom(r.RemoteAddr)
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
log.WithFields(log.Fields{"error": err.Error(), "remoteaddr": r.RemoteAddr}).Error("Error while parsing remote address")
host = ""
}
return user.allowedFrom(host)
}
+33
View File
@@ -0,0 +1,33 @@
package main
import (
"net/http"
"testing"
)
func TestUpdateAllowedFromIP(t *testing.T) {
userWithAllow := newACMETxt()
userWithAllow.AllowFrom = cidrslice{"192.168.1.2/32", "[::1]/128"}
userWithoutAllow := newACMETxt()
for i, test := range []struct {
remoteaddr string
expected bool
}{
{"192.168.1.2:1234", true},
{"192.168.1.1:1234", false},
{"invalid", false},
{"[::1]:4567", true},
} {
newreq, _ := http.NewRequest("GET", "/whatever", nil)
newreq.RemoteAddr = test.remoteaddr
ret := updateAllowedFromIP(newreq, userWithAllow)
if test.expected != ret {
t.Errorf("Test %d: Unexpected result for user with allowForm set", i)
}
if !updateAllowedFromIP(newreq, userWithoutAllow) {
t.Errorf("Test %d: Unexpected result for user without allowForm set", i)
}
}
}
+4
View File
@@ -36,6 +36,10 @@ connection = "/var/lib/acme-dns/acme-dns.db"
api_domain = ""
# listen ip eg. 127.0.0.1
ip = "0.0.0.0"
# disable registration endpoint
disable_registration = false
# autocert HTTP port, eg. 80 for answering Let's Encrypt HTTP-01 challenges. Mandatory if using tls = "letsencrypt".
autocert_port = "80"
# listen port, eg. 443 for default HTTPS
port = "80"
# possible values: "letsencrypt", "cert", "none"
+5 -1
View File
@@ -52,7 +52,11 @@ func answer(q dns.Question) ([]dns.RR, int, error) {
var rtype = q.Qtype
r, ok := RR.Records[rtype][domain]
if !ok {
rcode = dns.RcodeNameError
r, ok = RR.Records[dns.TypeCNAME][domain]
if !ok {
rcode = dns.RcodeNameError
}
}
log.WithFields(log.Fields{"qtype": dns.TypeToString[rtype], "domain": domain, "rcode": dns.RcodeToString[rcode]}).Debug("Answering question for domain")
return r, rcode, nil
+7 -2
View File
@@ -67,7 +67,9 @@ func startHTTPAPI() {
// Logwriter for saner log output
c.Log = stdlog.New(logwriter, "", 0)
}
api.POST("/register", webRegisterPost)
if !Config.API.DisableRegistration {
api.POST("/register", webRegisterPost)
}
api.POST("/update", Auth(webUpdatePost))
host := Config.API.IP + ":" + Config.API.Port
@@ -83,6 +85,9 @@ func startHTTPAPI() {
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(Config.API.Domain),
}
autocerthost := Config.API.IP + ":" + Config.API.AutocertPort
log.WithFields(log.Fields{"autocerthost": autocerthost, "domain": Config.API.Domain}).Debug("Opening HTTP port for autocert")
go http.ListenAndServe(autocerthost, m.HTTPHandler(nil))
cfg.GetCertificate = m.GetCertificate
srv := &http.Server{
Addr: host,
@@ -90,7 +95,7 @@ func startHTTPAPI() {
TLSConfig: cfg,
ErrorLog: stdlog.New(logwriter, "", 0),
}
log.WithFields(log.Fields{"host": host, "domain": Config.API.Domain}).Info("Listening HTTPS autocert")
log.WithFields(log.Fields{"host": host, "domain": Config.API.Domain}).Info("Listening HTTPS, using certificate from autocert")
log.Fatal(srv.ListenAndServeTLS("", ""))
case "cert":
srv := &http.Server{
+11 -9
View File
@@ -50,15 +50,17 @@ type dbsettings struct {
// API config
type httpapi struct {
Domain string `toml:"api_domain"`
IP string
Port string `toml:"port"`
TLS string
TLSCertPrivkey string `toml:"tls_cert_privkey"`
TLSCertFullchain string `toml:"tls_cert_fullchain"`
CorsOrigins []string
UseHeader bool `toml:"use_header"`
HeaderName string `toml:"header_name"`
Domain string `toml:"api_domain"`
IP string
DisableRegistration bool `toml:"disable_registration"`
AutocertPort string `toml:"autocert_port"`
Port string `toml:"port"`
TLS string
TLSCertPrivkey string `toml:"tls_cert_privkey"`
TLSCertFullchain string `toml:"tls_cert_fullchain"`
CorsOrigins []string
UseHeader bool `toml:"use_header"`
HeaderName string `toml:"header_name"`
}
// Logging config
+6
View File
@@ -38,6 +38,12 @@ func sanitizeString(s string) string {
return re.ReplaceAllString(s, "")
}
func sanitizeIPv6addr(s string) string {
// Remove brackets from IPv6 addresses, net.ParseCIDR needs this
re, _ := regexp.Compile("[\\[\\]]+")
return re.ReplaceAllString(s, "")
}
func generatePassword(length int) string {
ret := make([]byte, length)
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-_"