Remove CamliNetIP, camnetdns, gpgchallenge

These were all part of an earlier effort to let it be easy for people
to easily deploy their own Perkeep servers and get them on the
internet. The idea was that we'd run a small DNS server which would
map from GPG public keys to their rented cloud IPs which users would
prove to us with the gpgchallenge stuff.

The recently added Tailscale support (see 43f34e5cc5, #1668) makes
most of this redundant.

Also I'd stopped running this infrastructure ages ago and removed the
launcher code recently in b5823a65b9 (and disabled it in
c9f78d02ad). So this was all basically dead code.

Signed-off-by: Brad Fitzpatrick <brad@danga.com>
This commit is contained in:
Brad Fitzpatrick 2024-01-02 13:14:28 -08:00
parent 43f34e5cc5
commit 0caf36bc9c
14 changed files with 12 additions and 1770 deletions

View File

@ -44,18 +44,6 @@ JSON. It can either be in [simple mode](#simplemode) (for basic configurations),
from Let's Encrypt.
* As a fallback, if no FQDN is found, a self-signed certificate is generated.
* `camliNetIP`: the optional internet-facing IP address for this
Perkeep instance. If set, a name in the camlistore.net domain for
that IP address will be requested on startup. The obtained domain name
will then be used as the host name in the base URL.
For now, the protocol to get the name requires receiving a challenge
on port 443. Also, this option implies `https`, and that the HTTPS
certificate is obtained from [Let's Encrypt](https://letsencrypt.org).
For these reasons, this option is mutually exclusive with `baseURL`, `listen`,
`httpsCert`, and `httpsKey`.
On cloud instances (Google Compute Engine only for now), this option is
automatically used.
* `identity`: your GPG fingerprint. A keypair is created for new users on
start, but this may be changed if you know what you're doing.

2
go.mod
View File

@ -22,7 +22,6 @@ require (
github.com/lib/pq v1.10.2
github.com/mailgun/mailgun-go v0.0.0-20171127222028-17e8bd11e87c
github.com/mattn/go-mastodon v0.0.5-0.20190517015615-8f6192e26b66
github.com/miekg/dns v1.1.57
github.com/nf/cr2 v0.0.0-20140528043846-05d46fef4f2f
github.com/pkg/sftp v1.13.6
github.com/plaid/plaid-go v0.0.0-20161222051224-02b6af68061b
@ -111,6 +110,7 @@ require (
github.com/mdlayher/netlink v1.7.2 // indirect
github.com/mdlayher/sdnotify v1.0.0 // indirect
github.com/mdlayher/socket v0.5.0 // indirect
github.com/miekg/dns v1.1.57 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/pierrec/lz4/v4 v4.1.18 // indirect
github.com/pkg/errors v0.9.1 // indirect

16
make.go
View File

@ -52,7 +52,6 @@ var (
buildARM = flag.String("arm", "7", "ARM version to use if building for ARM. Note that this version applies even if the host arch is ARM too (and possibly of a different version).")
stampVersion = flag.Bool("stampversion", true, "Stamp version into buildinfo.GitInfo")
website = flag.Bool("website", false, "Just build the website.")
camnetdns = flag.Bool("camnetdns", false, "Just build perkeep.org/server/camnetdns.")
static = flag.Bool("static", false, "Build a static binary, so it can run in an empty container.")
offline = flag.Bool("offline", false, "Do not fetch the JS code for the web UI from perkeep.org. If not rebuilding the web UI, just trust the files on disk (if they exist).")
)
@ -73,10 +72,6 @@ func main() {
}
}
if *website && *camnetdns {
log.Fatal("-camnetdns and -website are mutually exclusive")
}
failIfCamlistoreOrgDir()
verifyGoModules()
verifyGoVersion()
@ -114,20 +109,13 @@ func main() {
if *website {
log.Fatal("--targets and --website are mutually exclusive")
}
if *camnetdns {
log.Fatal("--targets and --camnetdns are mutually exclusive")
}
if t := strings.Split(*targets, ","); len(t) != 0 {
targs = t
}
}
if *website || *camnetdns {
if *website {
buildAll = false
if *website {
targs = []string{"perkeep.org/website/pk-web"}
} else if *camnetdns {
targs = []string{"perkeep.org/server/camnetdns"}
}
targs = []string{"perkeep.org/website/pk-web"}
}
tags := []string{"purego"} // for cznic/zappy

View File

@ -1,93 +0,0 @@
/*
Copyright 2016 The Perkeep Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// The client command is an example client of the gpgchallenge package.
package main
import (
"crypto/tls"
"flag"
"fmt"
"log"
"net/http"
"time"
"golang.org/x/net/http2"
"perkeep.org/internal/httputil"
"perkeep.org/internal/osutil"
"perkeep.org/pkg/gpgchallenge"
)
var (
flagPort = flag.Int("p", 443, "port that the server will challenge us on.")
flagKeyRing = flag.String("keyring", osutil.DefaultSecretRingFile(), "path to the GPG keyring file")
flagKeyID = flag.String("keyid", "", "GPG key ID")
flagClaimedIP = flag.String("ip", "", "IP address to prove control over")
flagServer = flag.String("server", "camnetdns.camlistore.org", "server we want to run the challenge against")
)
func main() {
flag.Parse()
if *flagKeyID == "" {
log.Fatal("you need to specify -keyid")
}
if *flagClaimedIP == "" {
log.Fatal("you need to specify -ip")
}
gpgchallenge.ClientChallengedPort = *flagPort
cl, err := gpgchallenge.NewClient(
*flagKeyRing,
*flagKeyID,
*flagClaimedIP,
)
if err != nil {
log.Fatal(err)
}
config := &tls.Config{
NextProtos: []string{http2.NextProtoTLS, "http/1.1"},
MinVersion: tls.VersionTLS12,
}
selfCert, selfKey, err := httputil.GenSelfTLS(*flagClaimedIP + "-challenge")
if err != nil {
log.Fatalf("could no generate self-signed certificate: %v", err)
}
config.Certificates = make([]tls.Certificate, 1)
config.Certificates[0], err = tls.X509KeyPair(selfCert, selfKey)
if err != nil {
log.Fatalf("could not load TLS certificate: %v", err)
}
laddr := fmt.Sprintf(":%d", *flagPort)
l, err := tls.Listen("tcp", laddr, config)
if err != nil {
log.Fatalf("could not listen on %v for challenge: %v", laddr, err)
}
pattern, handler := cl.Handler()
http.Handle(pattern, handler)
errc := make(chan error, 1)
go func() {
errc <- http.Serve(l, handler)
}()
time.Sleep(time.Second)
if err := cl.Challenge(*flagServer); err != nil {
log.Fatal(err)
}
log.Printf("Challenge success")
}

View File

@ -1,830 +0,0 @@
/*
Copyright 2016 The Perkeep Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package gpgchallenge provides a Client and a Server so that a Client can
// prove ownership of an IP address by solving a GPG challenge sent by the Server
// at the claimed IP.
// The protocol is as follows:
//
// - The Client GETs a random token from the server, at the /token endpoint, and signs
// that token with its GPG private key (armor detached signature).
//
// - When it is ready[*], the client POSTs an application/x-www-form-urlencoded over
// HTTPS to the server, at the /claim endpoint. It sends the following URL-encoded
// values as the request body: its armor encoded public key as "pubkey", the IP
// address it's claiming as "challengeIP", the token it got from the server as "token",
// and the signature for the token as "signature".
//
// - The Server receives the claim. It verifies that the token (nonce) is indeed one that
// it had generated. It parses the client's public key. It verifies with that
// public key that the sent signature matches the token. The serve ACKs to the client.
//
// - The Server generates a random token, and POSTs it to the challenged IP
// (over HTTPS, with certificate verification disabled) at the /challenge endpoint.
//
// - The Client receives the random token, signs it (armored detached
// signature), and sends the signature as a reply.
//
// - The Server receives the signed token and verifies it with the Client's
// public key.
//
// - At this point, the challenge is successful, so the Server performs the
// action registered through the OnSuccess function.
//
// - The Server sends a last message to the Client at the /ack endpoint,
// depending on the result of the OnSuccess action. "ACK" if it was successful, the
// error message otherwise.
//
// [*]As the Server connects to the Client to challenge it, the Client must obviously
// have a way, which does not need to be described by the protocol, to listen to and
// accept these connections.
package gpgchallenge // import "perkeep.org/pkg/gpgchallenge"
import (
"bytes"
"context"
"crypto"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/crypto/openpgp"
"golang.org/x/crypto/openpgp/armor"
"golang.org/x/crypto/openpgp/packet"
"golang.org/x/time/rate"
"go4.org/wkfs"
)
// ClientChallengedPort is the port that the client will be challenged on by
// the server.
var ClientChallengedPort = 443
// SNISuffix is the Server Name Indication prefix used when dialing the
// client's handler. The SNI is challengeIP+SNISuffix.
const SNISuffix = "-gpgchallenge"
const (
clientEndPointChallenge = "challenge"
clientEndPointAck = "ack"
clientEndPointReady = "ready" // not part of the protocol, just to check if client has a listener
serverEndPointToken = "token"
serverEndPointChallenge = "claim"
// clientHandlerPrefix is the URL path prefix for all the client endpoints.
clientHandlerPrefix = "/.well-known/camlistore/gpgchallenge/"
nonceValidity = 10 * time.Second
spamDelay = 5 * time.Second // any repeated attempt under this delay is considered as spam
forgetSeen = time.Minute // anyone being quiet for that long is taken off the "potential spammer" list
queriesRate = 10 // max concurrent (non-whitelisted) clients
minKeySize = 2048 // in bits. to force potential attackers to generate GPG keys at least this expensive.
requestTimeout = 3 * time.Second // so a client does not make use create many long-lived connections
)
// Server sends a challenge when a client that wants to claim ownership of an IP
// address requests so. Server runs OnSuccess when the challenge is successful.
type Server struct {
// OnSuccess is user-defined, and it is run by the server upon
// successuful verification of the client's challenge. Identity is the
// short form of the client's public key's fingerprint in capital hex.
// Address is the IP address that the client was claiming.
OnSuccess func(identity, address string) error
once sync.Once // for initializing all the fields below in serverInit
// keyHMAC is the key for generating with HMAC, the random tokens for
// the clients. Each token is: message-HEX(HMAC(message)) , with message
// being the current unix time. This format allows the server to verify a
// token it gets back from the client was indeed generated by the server.
keyHMAC []byte
nonceUsedMu sync.Mutex
nonceUsed map[string]bool // whether such a nonce has already been sent back by the client
// All of the fields below are for rate-limiting or attacks suppression.
limiter *rate.Limiter
whiteListMu sync.Mutex
// whiteList contains clients which already successfully challenged us,
// and therefore are not subject to any rate-limiting. keyed by "keyID-IP".
whiteList map[string]struct{}
keyIDSeenMu sync.Mutex
keyIDSeen map[string]time.Time // last time we saw a keyID, for rate-limiting purposes.
IPSeenMu sync.Mutex
IPSeen map[string]time.Time // last time we saw a claimedIP, for rate-limiting purposes.
}
func (cs *Server) serverInit() error {
nonce, err := genNonce()
if err != nil {
return fmt.Errorf("error generating key for hmac: %v", err)
}
cs.keyHMAC = []byte(nonce)
cs.nonceUsed = make(map[string]bool)
cs.limiter = rate.NewLimiter(queriesRate, 1)
cs.whiteList = make(map[string]struct{})
cs.keyIDSeen = make(map[string]time.Time)
cs.IPSeen = make(map[string]time.Time)
return nil
}
func (cs *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
cs.once.Do(func() {
if err := cs.serverInit(); err != nil {
panic(fmt.Sprintf("Could not initialize server: %v", err))
}
})
if r.URL.Path == "/"+serverEndPointToken {
cs.handleNonce(w, r)
return
}
if r.URL.Path == "/"+serverEndPointChallenge {
cs.handleClaim(w, r)
return
}
http.Error(w, "nope", 404)
}
// handleNonce replies with a nonce. The nonce format is:
// message-HexOf(HMAC(message)), where message is the current unix time in seconds.
func (cs *Server) handleNonce(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "not a GET", http.StatusMethodNotAllowed)
return
}
mac := hmac.New(sha256.New, cs.keyHMAC)
message := fmt.Sprintf("%d", time.Now().Unix())
mac.Write([]byte(message))
messageHash := string(mac.Sum(nil))
nonce := fmt.Sprintf("%s-%x", message, messageHash)
if _, err := io.Copy(w, strings.NewReader(nonce)); err != nil {
log.Printf("error sending nonce: %v", err)
return
}
}
func (cs *Server) handleClaim(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "not a POST", http.StatusMethodNotAllowed)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, "could not parse claim form", 500)
return
}
pks := r.Form.Get("pubkey")
if len(pks) == 0 {
http.Error(w, "pubkey value not found in form", http.StatusBadRequest)
return
}
claimedIP := r.Form.Get("challengeIP")
if len(claimedIP) == 0 {
http.Error(w, "claimedIP value not found in form", http.StatusBadRequest)
return
}
token := r.Form.Get("token")
if len(token) == 0 {
http.Error(w, "token value not found in form", http.StatusBadRequest)
return
}
tokenSig := r.Form.Get("signature")
if len(tokenSig) == 0 {
http.Error(w, "signature value not found in form", http.StatusBadRequest)
return
}
if err := cs.validateToken(token); err != nil {
http.Error(w, "invalid token", http.StatusBadRequest)
log.Printf("Error validating token: %v", err)
return
}
pk, err := parsePubKey(strings.NewReader(pks))
if err != nil {
http.Error(w, "invalid public key", http.StatusBadRequest)
log.Printf("Error parsing client public key: %v", err)
return
}
keySize, err := pk.BitLength()
if err != nil {
http.Error(w, "could not check key size", 500)
log.Printf("could not check key size: %v", err)
return
}
if keySize < minKeySize {
http.Error(w, fmt.Sprintf("minimum key size is %d bits", minKeySize), http.StatusBadRequest)
return
}
if err := cs.validateTokenSignature(pk, token, tokenSig); err != nil {
http.Error(w, "invalid token signature", http.StatusBadRequest)
log.Printf("Error validating token signature: %v", err)
return
}
// Verify claimedIP looks ok
ip := net.ParseIP(claimedIP)
if ip == nil {
http.Error(w, "nope", http.StatusBadRequest)
log.Printf("%q does not look like a valid IP address", claimedIP)
return
}
if !ip.IsGlobalUnicast() {
http.Error(w, "nope", http.StatusBadRequest)
log.Printf("%q does not look like a nice IP", claimedIP)
return
}
keyID := pk.KeyIdString()
if isSpammer := cs.rateLimit(keyID, claimedIP); isSpammer {
http.Error(w, "don't be a spammer", http.StatusTooManyRequests)
return
}
// ACK to the client
w.WriteHeader(http.StatusNoContent)
nonce, err := genNonce()
if err != nil {
log.Print(err)
return
}
tr := &http.Transport{
DisableKeepAlives: true,
TLSClientConfig: &tls.Config{
ServerName: claimedIP + SNISuffix,
InsecureSkipVerify: true,
},
}
cl := &http.Client{
Transport: tr,
Timeout: requestTimeout,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := cl.Post(fmt.Sprintf("https://%s:%d%s%s", claimedIP, ClientChallengedPort, clientHandlerPrefix, clientEndPointChallenge),
"text/plain", strings.NewReader(nonce))
if err != nil {
log.Printf("Error while sending the challenge to the client: %v", err)
return
}
defer resp.Body.Close()
sig, err := cs.receiveSignedNonce(resp.Body)
if err != nil {
log.Printf("Error reading signed token: %v", err)
return
}
hash := sig.Hash.New()
hash.Write([]byte(nonce))
if err := pk.VerifySignature(hash, sig); err != nil {
log.Printf("Error verifying token signature: %v", err)
return
}
// Client is in the clear, so we add them to the whitelist for next time
// TODO(mpl): unbounded for now, but it would be easy to e.g. keep the
// time as value, and regularly remove very old entries. Or use a sized
// cache. etc.
cs.whiteListMu.Lock()
cs.whiteList[keyID+"-"+claimedIP] = struct{}{}
cs.whiteListMu.Unlock()
ackMessage := "ACK"
if err := cs.OnSuccess(pk.KeyIdShortString(), claimedIP); err != nil {
ackMessage = fmt.Sprintf("challenge successful, but could not perform operation: %v", err)
}
resp, err = cl.Post(fmt.Sprintf("https://%s:%d%s%s", claimedIP, ClientChallengedPort, clientHandlerPrefix, clientEndPointAck),
"text/plain", strings.NewReader(ackMessage))
if err != nil {
log.Printf("Error sending closing message: %v", err)
return
}
resp.Body.Close()
}
// rateLimit uses the cs.limiter to make sure that any client that hasn't
// previously successfully challenged us is rate limited. It also keeps track of
// clients that haven't successfully challenged, and it returns true if such a
// client should be considered a spammer.
func (cs *Server) rateLimit(keyID, claimedIP string) (isSpammer bool) {
cs.whiteListMu.Lock()
if _, ok := cs.whiteList[keyID+"-"+claimedIP]; ok {
cs.whiteListMu.Unlock()
return false
}
cs.whiteListMu.Unlock()
// If they haven't successfully challenged us before, they look suspicious.
cs.keyIDSeenMu.Lock()
lastSeen, ok := cs.keyIDSeen[keyID]
// always keep track of the last time we saw them
cs.keyIDSeen[keyID] = time.Now()
cs.keyIDSeenMu.Unlock()
time.AfterFunc(forgetSeen, func() {
// but everyone get a clean slate after a minute of being quiet
cs.keyIDSeenMu.Lock()
delete(cs.keyIDSeen, keyID)
cs.keyIDSeenMu.Unlock()
})
if ok {
// if we've seen their keyID before, they look even more suspicious, so investigate.
if lastSeen.Add(spamDelay).After(time.Now()) {
// we kick them out if we saw them less than 5 seconds ago.
return true
}
}
cs.IPSeenMu.Lock()
lastSeen, ok = cs.IPSeen[claimedIP]
// always keep track of the last time we saw them
cs.IPSeen[claimedIP] = time.Now()
cs.IPSeenMu.Unlock()
time.AfterFunc(forgetSeen, func() {
// but everyone get a clean slate after a minute of being quiet
cs.IPSeenMu.Lock()
delete(cs.IPSeen, claimedIP)
cs.IPSeenMu.Unlock()
})
if ok {
// if we've seen their IP before, they look even more suspicious, so investigate.
if lastSeen.Add(spamDelay).After(time.Now()) {
// we kick them out if we saw them less than 5 seconds ago.
return true
}
}
// global rate limit that applies to all strangers at the same time
cs.limiter.Wait(context.Background())
return false
}
func genNonce() (string, error) {
buf := make([]byte, 20)
if n, err := rand.Read(buf); err != nil || n != len(buf) {
return "", fmt.Errorf("failed to generate random nonce: %v", err)
}
return fmt.Sprintf("%x", buf), nil
}
func (cs *Server) validateToken(token string) error {
// Check the token is one of ours, and not too old
parts := strings.Split(token, "-")
if len(parts) != 2 {
return fmt.Errorf("client sent back an invalid token")
}
nonce := parts[0]
nonceTimeSeconds, err := strconv.ParseInt(nonce, 10, 64)
if err != nil {
return fmt.Errorf("time in nonce could not be parsed: %v", err)
}
nonceTime := time.Unix(nonceTimeSeconds, 0)
if nonceTime.Add(nonceValidity).Before(time.Now()) {
return fmt.Errorf("client sent back an expired nonce")
}
mac := hmac.New(sha256.New, cs.keyHMAC)
mac.Write([]byte(nonce))
expectedMAC, err := hex.DecodeString(parts[1])
if err != nil {
return fmt.Errorf("could not decode HMAC %q in nonce: %v", parts[1], err)
}
if !hmac.Equal(mac.Sum(nil), expectedMAC) {
return fmt.Errorf("client sent back a nonce we did not generate")
}
cs.nonceUsedMu.Lock()
if used, _ := cs.nonceUsed[nonce]; used {
log.Printf("nonce %q has already been received", nonce)
return nil
}
cs.nonceUsed[nonce] = true
cs.nonceUsedMu.Unlock()
time.AfterFunc(nonceValidity, func() {
cs.nonceUsedMu.Lock()
defer cs.nonceUsedMu.Unlock()
delete(cs.nonceUsed, nonce)
})
return nil
}
func (cs *Server) validateTokenSignature(pk *packet.PublicKey, token, tokenSig string) error {
sig, err := cs.receiveSignedNonce(strings.NewReader(tokenSig))
if err != nil {
return err
}
hash := sig.Hash.New()
hash.Write([]byte(token))
return pk.VerifySignature(hash, sig)
}
func parsePubKey(r io.Reader) (*packet.PublicKey, error) {
block, _ := armor.Decode(r)
if block == nil {
return nil, errors.New("can't parse armor")
}
var p packet.Packet
var err error
p, err = packet.Read(block.Body)
if err != nil {
return nil, err
}
pk, ok := p.(*packet.PublicKey)
if !ok {
return nil, errors.New("PGP packet isn't a public key")
}
return pk, nil
}
func (cs *Server) receiveSignedNonce(r io.Reader) (*packet.Signature, error) {
block, _ := armor.Decode(r)
if block == nil {
return nil, errors.New("can't parse armor")
}
var p packet.Packet
var err error
p, err = packet.Read(block.Body)
if err != nil {
return nil, err
}
sig, ok := p.(*packet.Signature)
if !ok {
return nil, errors.New("PGP packet isn't a signature packet")
}
if sig.Hash != crypto.SHA1 && sig.Hash != crypto.SHA256 {
return nil, errors.New("can only verify SHA1 or SHA256 signatures")
}
if sig.SigType != packet.SigTypeBinary {
return nil, errors.New("can only verify binary signatures")
}
return sig, nil
}
// Client is used to prove ownership of an IP address, by answering a GPG
// challenge that the server sends at the address.
// A client must first register its Handler with an HTTPS server, before it can
// perform the challenge.
type Client struct {
keyRing, keyId string
signer *openpgp.Entity
challengeIP string
handler http.Handler
// any error from one of the HTTP handle func is sent through errc, so
// it can be communicated to the Challenge method, which can then error out
// accordingly.
errc chan error
mu sync.Mutex
challengeDone bool
}
// NewClient returns a Client. keyRing and keyId are the GPG key ring and key ID
// used to fulfill the challenge. challengeIP is the address that client proves
// that it owns, by answering the challenge the server sends at this address.
func NewClient(keyRing, keyId, challengeIP string) (*Client, error) {
signer, err := secretKeyEntity(keyRing, keyId)
if err != nil {
return nil, fmt.Errorf("could not get signer %v from keyRing %v: %v", keyId, keyRing, err)
}
cl := &Client{
keyRing: keyRing,
keyId: keyId,
signer: signer,
challengeIP: challengeIP,
errc: make(chan error, 1),
}
handler := &clientHandler{
cl: cl,
}
cl.handler = handler
return cl, nil
}
// Handler returns the client's handler, that should be registered with an HTTPS
// server for the returned prefix, for the client to be able to receive the
// challenge.
func (cl *Client) Handler() (prefix string, h http.Handler) {
return clientHandlerPrefix, cl.handler
}
// clientHandler is the "server" part of the Client, so it can receive and
// answer the Server's challenge.
type clientHandler struct {
cl *Client
}
func (h *clientHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.cl.mu.Lock()
defer h.cl.mu.Unlock()
if r.URL.Path == clientHandlerPrefix+clientEndPointReady {
h.handleReady(w, r)
return
}
if r.URL.Path == clientHandlerPrefix+clientEndPointChallenge {
h.handleChallenge(w, r)
return
}
if r.URL.Path == clientHandlerPrefix+clientEndPointAck {
h.handleACK(w, r)
return
}
http.Error(w, "wrong path", 404)
}
// Challenge requests a challenge from the server running at serverAddr, which
// should be a host name or of the hostname:port form, and then fulfills that challenge.
func (cl *Client) Challenge(serverAddr string) error {
if err := cl.listenSelfCheck(serverAddr); err != nil {
return err
}
return cl.challenge(serverAddr)
}
// listenSelfCheck tests whether the client is ready to receive a challenge,
// i.e. that the caller has registered the client's handler with a server.
func (cl *Client) listenSelfCheck(serverAddr string) error {
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
ServerName: cl.challengeIP + SNISuffix,
},
}
httpClient := &http.Client{
Transport: tr,
}
errc := make(chan error, 1)
respc := make(chan *http.Response, 1)
var err error
var resp *http.Response
go func() {
resp, err := httpClient.PostForm(fmt.Sprintf("https://localhost:%d%s%s", ClientChallengedPort, clientHandlerPrefix, clientEndPointReady),
url.Values{"server": []string{serverAddr}})
errc <- err
respc <- resp
}()
timeout := time.NewTimer(time.Second)
defer timeout.Stop()
select {
case err = <-errc:
resp = <-respc
case <-timeout.C:
return errors.New("The client needs an HTTPS listener for its handler to answer the server's challenge. You need to call Handler and register the http.Handler with an HTTPS server, before calling Challenge.")
}
if err != nil {
return fmt.Errorf("error starting challenge: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("error starting challenge: %v", resp.Status)
}
return nil
}
func (cl *Client) challenge(serverAddr string) error {
token, err := cl.getToken(serverAddr)
if err != nil {
return fmt.Errorf("could not get token from server: %v", err)
}
errc := make(chan error, 1)
go func() {
if err := cl.sendClaim(serverAddr, token); err != nil {
errc <- fmt.Errorf("error sending challenge claim to server: %v", err)
}
}()
timeout := time.NewTimer(10 * time.Second)
defer timeout.Stop()
select {
case err := <-errc:
return err
case err := <-cl.errc:
return err // nil here on success.
case <-timeout.C:
return errors.New("challenge timeout")
}
}
func (h *clientHandler) handleReady(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "not a POST", http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *clientHandler) handleChallenge(w http.ResponseWriter, r *http.Request) {
cl := h.cl
var stickyErr error
defer func() {
if stickyErr != nil {
cl.errc <- stickyErr
}
}()
if cl.challengeDone {
stickyErr = errors.New("challenge already answered")
http.Error(w, stickyErr.Error(), 500)
return
}
if r.Method != "POST" {
stickyErr = errors.New("not a POST")
http.Error(w, stickyErr.Error(), http.StatusMethodNotAllowed)
return
}
nonce, err := io.ReadAll(r.Body)
if err != nil {
stickyErr = err
http.Error(w, err.Error(), 500)
return
}
var buf bytes.Buffer
if err := openpgp.ArmoredDetachSign(
&buf,
cl.signer,
bytes.NewReader(nonce),
nil,
); err != nil {
stickyErr = err
http.Error(w, err.Error(), 500)
return
}
if _, err := io.Copy(w, &buf); err != nil {
stickyErr = fmt.Errorf("could not reply to challenge: %v", err)
return
}
cl.challengeDone = true
}
func (h *clientHandler) handleACK(w http.ResponseWriter, r *http.Request) {
cl := h.cl
var stickyErr error
defer func() {
cl.errc <- stickyErr
}()
if r.Method != "POST" {
stickyErr = errors.New("not a POST")
http.Error(w, stickyErr.Error(), http.StatusMethodNotAllowed)
return
}
if !cl.challengeDone {
stickyErr = errors.New("ACK received before challenge was over")
http.Error(w, stickyErr.Error(), http.StatusBadRequest)
return
}
ack, err := io.ReadAll(r.Body)
if err != nil {
stickyErr = err
http.Error(w, err.Error(), 500)
return
}
if string(ack) != "ACK" {
stickyErr = fmt.Errorf("unexpected ACK message from server: %q", string(ack))
http.Error(w, stickyErr.Error(), http.StatusBadRequest)
return
}
// reset it for reuse of the client.
cl.challengeDone = false
if _, err := io.Copy(w, strings.NewReader("OK")); err != nil {
log.Printf("non-critical error: could not reply to ACK: %v", err)
return
}
}
func (cl *Client) getToken(serverAddr string) (string, error) {
resp, err := http.Get(fmt.Sprintf("https://%s/%s", serverAddr, serverEndPointToken))
if err != nil {
return "", err
}
defer resp.Body.Close()
data, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
token := string(data)
if len(token) == 0 {
return "", errors.New("error getting initial token from server")
}
return token, nil
}
func (cl *Client) sendClaim(server, token string) error {
pubkey, err := armorPubKey(cl.keyRing, cl.keyId)
if err != nil {
return err
}
var buf bytes.Buffer
if err := openpgp.ArmoredDetachSign(
&buf,
cl.signer,
strings.NewReader(token),
nil,
); err != nil {
return fmt.Errorf("could not sign token: %v", err)
}
values := url.Values{
"pubkey": {string(pubkey)},
"challengeIP": {cl.challengeIP},
"token": {token},
"signature": {buf.String()},
}
resp, err := http.PostForm(fmt.Sprintf("https://%s/%s", server, serverEndPointChallenge), values)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent {
msg, err := io.ReadAll(resp.Body)
if err == nil {
return fmt.Errorf("unexpected claim response: %v, %v", resp.Status, string(msg))
}
return fmt.Errorf("unexpected claim response: %v", resp.Status)
}
return nil
}
func armorPubKey(keyRing string, keyId string) ([]byte, error) {
pubkey, err := publicKeyEntity(keyRing, keyId)
if err != nil {
return nil, err
}
var buf bytes.Buffer
wc, err := armor.Encode(&buf, openpgp.PublicKeyType, nil)
if err != nil {
return nil, err
}
if err := pubkey.Serialize(wc); err != nil {
return nil, err
}
if err := wc.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func publicKeyEntity(keyRing string, keyId string) (*openpgp.Entity, error) {
f, err := wkfs.Open(keyRing)
if err != nil {
return nil, fmt.Errorf("could not open keyRing %v: %v", keyRing, err)
}
defer f.Close()
el, err := openpgp.ReadKeyRing(f)
if err != nil {
return nil, err
}
for _, e := range el {
pubk := e.PrimaryKey
if pubk.KeyIdString() == keyId {
return e, nil
}
}
return nil, fmt.Errorf("keyId %v not found in %v", keyId, keyRing)
}
func secretKeyEntity(keyRing string, keyId string) (*openpgp.Entity, error) {
f, err := wkfs.Open(keyRing)
if err != nil {
return nil, fmt.Errorf("could not open keyRing %v: %v", keyRing, err)
}
defer f.Close()
el, err := openpgp.ReadKeyRing(f)
if err != nil {
return nil, err
}
for _, e := range el {
pubk := &e.PrivateKey.PublicKey
// TODO(mpl): decrypt private key if it is passphrase-encrypted
if pubk.KeyIdString() == keyId {
return e, nil
}
}
return nil, fmt.Errorf("keyId %v not found in %v", keyId, keyRing)
}

View File

@ -1,57 +0,0 @@
/*
Copyright 2016 The Perkeep Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// The server command is an example server of the gpgchallenge package.
package main
import (
"flag"
"log"
"net/http"
"perkeep.org/pkg/gpgchallenge"
)
var (
flagHost = flag.String("host", "", "host:port to listen on for https connections")
flagTLSCert = flag.String("cert", "", "TLS certificate file")
flagTLSKey = flag.String("key", "", "TLS key file")
flagClientPort = flag.Int("client_port", 443, "the port to challenge the client on")
)
func main() {
flag.Parse()
if *flagHost == "" {
log.Fatal("you need to specify -host")
}
if *flagTLSCert == "" {
log.Fatal("you need to specify -cert")
}
if *flagTLSKey == "" {
log.Fatal("you need to specify -key")
}
gpgchallenge.ClientChallengedPort = *flagClientPort
cs := &gpgchallenge.Server{
OnSuccess: func(identity, address string) error {
// This is where and when camnetdns would add the name entry.
log.Printf("Server says challenge is success for %v at %v", identity, address)
return nil
},
}
log.Fatal(http.ListenAndServeTLS(*flagHost, *flagTLSCert, *flagTLSKey, cs))
}

View File

@ -28,15 +28,7 @@ import (
"cloud.google.com/go/compute/metadata"
)
// For getting a name in camlistore.net
const (
// CamliNetDNS is the hostname of the camlistore.net DNS server.
CamliNetDNS = "camnetdns.camlistore.org"
// CamliNetDomain is the camlistore.net domain name. It is relevant to
// Perkeep, because a deployment through the Perkeep on Google Cloud launcher
// automatically offers a subdomain name in this domain to any instance.
CamliNetDomain = "camlistore.net"
// useDBNamesConfig is a sentinel value for DBUnique to indicate that we want the
// low-level configuration generator to keep on using the old DBNames
// style configuration for database names.
@ -83,7 +75,6 @@ func DefaultEnvConfig() (*Config, error) {
ShareHandler: true,
}
externalIP, _ := metadata.ExternalIP()
hostName, _ := metadata.InstanceAttributeValue("camlistore-hostname")
// If they specified a hostname (previously common with old pk-deploy), then:
// if it looks like an FQDN, perkeepd is going to rely on Let's
@ -93,11 +84,11 @@ func DefaultEnvConfig() (*Config, error) {
// exactly as if the instance had no hostname, so that it registers its hostname/IP
// with the camlistore.net DNS server (possibly needlessly, if the instance IP has
// not changed) again.
if hostName != "" && !strings.HasSuffix(hostName, CamliNetDomain) {
if hostName != "" && !strings.HasSuffix(hostName, "camlistore.net") {
highConf.BaseURL = fmt.Sprintf("https://%s", hostName)
highConf.Listen = "0.0.0.0:443"
} else {
highConf.CamliNetIP = externalIP
panic("unsupported legacy configuration using camlistore.net is no longer supported")
}
// Detect a linked Docker MySQL container. It must have alias "mysqldb".

View File

@ -974,15 +974,6 @@ func (b *lowBuilder) genLowLevelPrefixes() error {
func (b *lowBuilder) build() (*Config, error) {
conf, low := b.high, b.low
if conf.CamliNetIP != "" {
if !conf.HTTPS {
return nil, errors.New("CamliNetIP requires HTTPS")
}
if conf.HTTPSCert != "" || conf.HTTPSKey != "" || conf.Listen != "" || conf.BaseURL != "" {
return nil, errors.New("CamliNetIP is mutually exclusive with HTTPSCert, HTTPSKey, Listen, and BaseURL")
}
low["camliNetIP"] = conf.CamliNetIP
}
if conf.HTTPS {
if (conf.HTTPSCert != "") != (conf.HTTPSKey != "") {
return nil, errors.New("Must set both httpsCert and httpsKey (or neither to generate a self-signed cert)")

View File

@ -411,7 +411,6 @@ func handlerTypeWantsAuth(handlerType string) bool {
type Config struct {
jconf jsonconfig.Obj // low-level JSON config
camliNetIP string // optional
httpsCert string // optional
httpsKey string // optional
https bool
@ -442,11 +441,6 @@ func (c *Config) UIPath() string {
return c.uiPath
}
// CamliNetIP returns the optional IP address that this server can be
// reached out. If set in the config, the server will request a DNS
// subdomain name from the Perkeep camlistore.net DNS server.
func (c *Config) CamliNetIP() string { return c.camliNetIP }
// BaseURL returns the optional URL prefix listening the root of this server.
// It does not end in a trailing slash.
func (c *Config) BaseURL() string { return c.baseURL }
@ -577,7 +571,6 @@ func load(filename string, opener func(filename string) (jsonconfig.File, error)
// readFields reads the low-level jsonconfig fields using the jsonconfig package
// and copies them into c. This marks them as known fields before a future call to InstallerHandlers
func (c *Config) readFields() error {
c.camliNetIP = c.jconf.OptionalString("camliNetIP", "")
c.listenAddr = c.jconf.OptionalString("listen", "")
c.baseURL = strings.TrimSuffix(c.jconf.OptionalString("baseURL", ""), "/")
c.httpsCert = c.jconf.OptionalString("httpsCert", "")

View File

@ -101,23 +101,13 @@ func GenerateClientConfig(serverConfig jsonconfig.Obj) (*Config, error) {
return missingConfig(param)
}
var listen, baseURL string
camliNetIP := serverConfig.OptionalString("camliNetIP", "")
if camliNetIP != "" {
listen = ":443"
// TODO(mpl): move the camliNetDomain const from perkeepd.go
// to somewhere importable, so we can use it here. but later.
camliNetDomain := "camlistore.net"
baseURL = fmt.Sprintf("https://%s.%s/", keyID, camliNetDomain)
} else {
listen = serverConfig.OptionalString("listen", "")
baseURL = serverConfig.OptionalString("baseURL", "")
if listen == "" {
listen = baseURL
}
if listen == "" {
return nil, errors.New("required value for 'listen' or 'baseURL' not found")
}
listen := serverConfig.OptionalString("listen", "")
baseURL := serverConfig.OptionalString("baseURL", "")
if listen == "" {
listen = baseURL
}
if listen == "" {
return nil, errors.New("required value for 'listen' or 'baseURL' not found")
}
https := serverConfig.OptionalBool("https", false)

View File

@ -32,16 +32,6 @@ type Config struct {
BaseURL string `json:"baseURL,omitempty"` // Base URL the server advertises. For when behind a proxy.
Listen string `json:"listen"` // address (of the form host|ip:port) on which the server will listen on.
// CamliNetIP is the optional internet-facing IP address for this
// Perkeep instance. If set, a name in the camlistore.net domain for
// that IP address will be requested on startup. The obtained domain name
// will then be used as the host name in the base URL.
// For now, the protocol to get the name requires receiving a challenge
// on port 443. Also, this option implies HTTPS, and that the HTTPS
// certificate is obtained from Let's Encrypt. For these reasons, this
// option is mutually exclusive with BaseURL, Listen, HTTPSCert, and
// HTTPSKey.
CamliNetIP string `json:"camliNetIP"`
Identity string `json:"identity"` // GPG identity.
IdentitySecretRing string `json:"identitySecretRing"` // path to the secret ring file.

View File

@ -1,532 +0,0 @@
/*
Copyright 2016 The Perkeep Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// The camnetdns server serves camlistore.net's DNS server and its
// DNS challenges
package main
import (
"context"
"crypto/rand"
"crypto/tls"
"errors"
"flag"
"fmt"
"log"
"net"
"net/http"
"strings"
"time"
"perkeep.org/internal/lru"
"perkeep.org/internal/osutil"
"perkeep.org/pkg/gpgchallenge"
"perkeep.org/pkg/sorted"
"cloud.google.com/go/compute/metadata"
"cloud.google.com/go/datastore"
"cloud.google.com/go/logging"
"github.com/miekg/dns"
"go4.org/cloud/cloudlaunch"
"golang.org/x/crypto/acme/autocert"
"golang.org/x/net/http2"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
compute "google.golang.org/api/compute/v1"
)
var (
addr = flag.String("addr", defaultListenAddr(), "specify address for server to listen on")
flagServerIP = flag.String("server_ip", "104.154.231.160", "The IP address of the authoritative name server for camlistore.net, i.e. the address where this program will run.")
)
var launchConfig = &cloudlaunch.Config{
Name: "camnetdns",
BinaryBucket: "camlistore-dnsserver-resource",
GCEProjectID: GCEProjectID,
Scopes: []string{
compute.ComputeScope,
logging.WriteScope,
datastore.ScopeDatastore,
},
}
const (
GCEProjectID = "camlistore-website"
// DefaultResponseTTL is the record TTL in seconds
DefaultResponseTTL = 300
// even if record already existed in store, we overwrite it if it is older than 30 days, for analytics.
staleRecord = 30 * 24 * time.Hour
// max number of records in the lru cache
cacheSize = 1e6
// stagingCamwebHost is the FQDN of the staging version of the
// Perkeep website. We handle it differently from the rest as:
// 1) we discover its IP using the GCE API, 2) we only trust the stored
// version of it for 5 minutes.
stagingCamwebHost = "staging.camlistore.net."
lowTTL = 300 // in seconds
)
var (
errRecordNotFound = errors.New("record not found")
// lastCamwebUpdate is the last time we updated the stored value for stagingCamwebHost
lastCamwebUpdate time.Time
)
func defaultListenAddr() string {
if metadata.OnGCE() {
return ":53"
}
return ":5300"
}
type keyValue interface {
// Get fetches the value for key. It returns errRecordNotFound when
// there is no such record.
Get(key string) (string, error)
Set(key, value string) error
}
// cachedStore is a keyValue implementation that stores in Google's datastore
// with dsClient. It automatically stores to cache as well on writes, and always
// tries to read from cache first.
type cachedStore struct {
// datastore client to store the records. It should not be nil.
dsClient *datastore.Client
// cache stores the most recent records. It should not be nil.
cache *lru.Cache
}
// dsValue is the value type written to the datastore
type dsValue struct {
// Record is the RHS of an A or AAAA DNS record, i.e. an IPV4 or IPV6
// address.
Record string
// Updated is the last time this key value pair was inserted. Values
// older than 30 days are rewritten on writes.
Updated time.Time
}
func (cs cachedStore) Get(key string) (string, error) {
val, ok := cs.cache.Get(key)
if ok {
return val.(string), nil
}
// Cache Miss. hit the datastore.
ctx := context.Background()
dk := datastore.NameKey("camnetdns", key, nil)
var value dsValue
if err := cs.dsClient.Get(ctx, dk, &value); err != nil {
if err != datastore.ErrNoSuchEntity {
return "", fmt.Errorf("error getting value for %q from datastore: %v", key, err)
}
return "", errRecordNotFound
}
// And cache it.
cs.cache.Add(key, value.Record)
return value.Record, nil
}
func (cs cachedStore) put(ctx context.Context, key, value string) error {
dk := datastore.NameKey("camnetdns", key, nil)
val := &dsValue{
Record: value,
Updated: time.Now(),
}
if _, err := cs.dsClient.Put(ctx, dk, val); err != nil {
return fmt.Errorf("error writing (%q : %q) record to datastore: %v", key, value, err)
}
// and cache it.
cs.cache.Add(key, value)
return nil
}
// Set writes the key, value pair to cs. But it does not actually write if the
// value already exists, is up to date, and is more recent than 30 days.
func (cs cachedStore) Set(key, value string) error {
// check if record already exists
ctx := context.Background()
dk := datastore.NameKey("camnetdns", key, nil)
var oldValue dsValue
if err := cs.dsClient.Get(ctx, dk, &oldValue); err != nil {
if err != datastore.ErrNoSuchEntity {
return fmt.Errorf("error checking if record exists for %q from datastore: %v", key, err)
}
// record does not exist, write it.
return cs.put(ctx, key, value)
}
// record already exists
if oldValue.Record != value {
// new record is different, overwrite old one.
return cs.put(ctx, key, value)
}
// record is the same as before
if oldValue.Updated.Add(staleRecord).After(time.Now()) {
// record is still fresh, nothing to do.
return nil
}
// record is older than 30 days, so we rewrite it, for analytics.
return cs.put(ctx, key, value)
}
type memkv struct {
skv sorted.KeyValue
}
func (kv memkv) Get(key string) (string, error) {
val, err := kv.skv.Get(key)
if err != nil {
if err != sorted.ErrNotFound {
return "", err
}
return "", errRecordNotFound
}
return val, nil
}
func (kv memkv) Set(key, value string) error {
return kv.skv.Set(key, value)
}
// DNSServer implements the dns.Handler interface to serve A and AAAA
// records, using a KeyValue store for the lookups.
type DNSServer struct {
dataSource keyValue
}
func newDNSServer(src keyValue) *DNSServer {
return &DNSServer{
dataSource: src,
}
}
func (ds *DNSServer) HandleLookup(name string) (string, error) {
// Lowercase it all, to satisfy https://tools.ietf.org/html/draft-vixie-dnsext-dns0x20-00
loName := strings.ToLower(name)
if loName != stagingCamwebHost {
return ds.dataSource.Get(loName)
}
if time.Now().Before(lastCamwebUpdate.Add(lowTTL * time.Second)) {
return ds.dataSource.Get(loName)
}
stagingIP, err := stagingCamwebIP()
if err != nil {
log.Printf("Could not get new IP of %v: %v. Serving old value instead.", stagingCamwebHost, err)
return ds.dataSource.Get(loName)
}
if err := ds.dataSource.Set(stagingCamwebHost, stagingIP); err != nil {
log.Printf("Could not update (%v, %v) entry: %v", stagingCamwebHost, stagingIP, err)
} else {
lastCamwebUpdate = time.Now()
log.Printf("%v -> %v updated successfully", stagingCamwebHost, stagingIP)
}
return stagingIP, nil
}
const (
domain = "camlistore.net."
authorityNS = "camnetdns.camlistore.org."
// Increment after every change with format YYYYMMDDnn.
soaSerial = 2017012301
)
var (
authoritySection = &dns.NS{
Ns: authorityNS,
Hdr: dns.RR_Header{
Name: domain,
Rrtype: dns.TypeNS,
Class: dns.ClassINET,
Ttl: DefaultResponseTTL,
},
}
additionalSection = &dns.A{
A: net.ParseIP(*flagServerIP),
Hdr: dns.RR_Header{
Name: authorityNS,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: DefaultResponseTTL,
},
}
startOfAuthoritySection = &dns.SOA{
Hdr: dns.RR_Header{
Name: domain,
Rrtype: dns.TypeSOA,
Class: dns.ClassINET,
Ttl: DefaultResponseTTL,
},
Ns: authorityNS,
Mbox: "admin.camlistore.org.",
Serial: soaSerial,
Refresh: 3600, // TODO(mpl): set them lower once we got everything right.
Retry: 3600,
Expire: 86500,
Minttl: DefaultResponseTTL,
}
)
func commonHeader(q dns.Question) dns.RR_Header {
return dns.RR_Header{
Name: q.Name,
Rrtype: q.Qtype,
Class: dns.ClassINET,
Ttl: DefaultResponseTTL,
}
}
func (ds *DNSServer) ServeDNS(rw dns.ResponseWriter, mes *dns.Msg) {
resp := new(dns.Msg)
if mes.IsEdns0() != nil {
// Because apparently, if we're not going to handle EDNS
// properly, i.e. by returning an OPT section as well, we should
// return an RcodeFormatError. Not seen in the RFC, but doing that
// addresses some of the warnings from
// http://dnsviz.net/d/granivo.re/dnssec/
log.Print("unhandled EDNS message\n")
resp.SetRcode(mes, dns.RcodeFormatError)
if err := rw.WriteMsg(resp); err != nil {
log.Printf("error responding to DNS query: %s", err)
}
return
}
resp.SetReply(mes)
// TODO(mpl): Should we make sure that at least q.Name ends in
// "camlistore.net" before claiming we're authoritative on that response?
resp.Authoritative = true
for _, q := range mes.Question {
log.Printf("DNS request from %s: %s", rw.RemoteAddr(), &q)
answer, err := ds.HandleLookup(q.Name)
if err == errRecordNotFound {
resp.SetRcode(mes, dns.RcodeNameError)
if err := rw.WriteMsg(resp); err != nil {
log.Printf("error responding to DNS query: %s", err)
}
return
}
if err != nil {
log.Printf("error looking up %q: %v", q.Name, err)
continue
}
if q.Qclass != dns.ClassINET {
log.Printf("error: got invalid DNS question class %d\n", q.Qclass)
continue
}
switch q.Qtype {
// As long as we send a reply (even an empty one), we apparently
// look compliant. Or at least more than if we replied with
// RcodeNotImplemented.
case dns.TypeDNSKEY, dns.TypeTXT, dns.TypeMX:
break
case dns.TypeSOA:
resp.Answer = []dns.RR{startOfAuthoritySection}
resp.Extra = []dns.RR{additionalSection}
case dns.TypeNS:
resp.Answer = []dns.RR{authoritySection}
resp.Extra = []dns.RR{additionalSection}
case dns.TypeCAA:
header := commonHeader(q)
rr := &dns.CAA{
Hdr: header,
Flag: 1,
Tag: "issue",
Value: "letsencrypt.org",
}
resp.Answer = []dns.RR{rr}
case dns.TypeA, dns.TypeAAAA:
val := answer
ip := net.ParseIP(val)
// TODO(mpl): maybe we should have a distinct memstore for each type?
isIP6 := strings.Contains(ip.String(), ":")
header := commonHeader(q)
if strings.ToLower(q.Name) == stagingCamwebHost {
header.Ttl = lowTTL
}
var rr dns.RR
if q.Qtype == dns.TypeA {
if isIP6 {
break
}
rr = &dns.A{
A: ip,
Hdr: header,
}
} else if q.Qtype == dns.TypeAAAA {
if !isIP6 {
break
}
rr = &dns.AAAA{
AAAA: ip,
Hdr: header,
}
} else {
panic("unreachable")
}
resp.Answer = []dns.RR{rr}
// Not necessary, but I think they help.
resp.Ns = []dns.RR{authoritySection}
resp.Extra = []dns.RR{additionalSection}
default:
log.Printf("unhandled qtype: %d\n", q.Qtype)
resp.SetRcode(mes, dns.RcodeNotImplemented)
if err := rw.WriteMsg(resp); err != nil {
log.Printf("error responding to DNS query: %s", err)
}
return
}
break
}
if err := rw.WriteMsg(resp); err != nil {
log.Printf("error responding to DNS query: %s", err)
}
}
func stagingCamwebIP() (string, error) {
const (
projectID = "camlistore-website"
instName = "camweb-staging"
zone = "us-central1-f"
)
hc, err := google.DefaultClient(oauth2.NoContext)
if err != nil {
return "", fmt.Errorf("error getting an http client: %v", err)
}
s, err := compute.New(hc)
if err != nil {
return "", fmt.Errorf("error getting compute service: %v", err)
}
inst, err := compute.NewInstancesService(s).Get(projectID, zone, instName).Do()
if err != nil {
return "", fmt.Errorf("error getting instance: %v", err)
}
for _, netInt := range inst.NetworkInterfaces {
for _, ac := range netInt.AccessConfigs {
if ac.Type != "ONE_TO_ONE_NAT" {
continue
}
return ac.NatIP, nil
}
}
return "", errors.New("not found")
}
func main() {
launchConfig.MaybeDeploy()
flag.Parse()
var kv keyValue
var httpsListenAddr string
if metadata.OnGCE() {
httpsListenAddr = ":443"
dsClient, err := datastore.NewClient(context.Background(), GCEProjectID)
if err != nil {
log.Fatalf("Error creating datastore client for records: %v", err)
}
kv = cachedStore{
dsClient: dsClient,
cache: lru.New(cacheSize),
}
} else {
httpsListenAddr = ":4430"
kv = memkv{skv: sorted.NewMemoryKeyValue()}
}
if err := kv.Set("6401800c.camlistore.net.", "159.203.246.79"); err != nil {
log.Fatalf("Error adding %v:%v record: %v", "6401800c.camlistore.net.", "159.203.246.79", err)
}
if err := kv.Set(domain, *flagServerIP); err != nil {
log.Fatalf("Error adding %v:%v record: %v", domain, *flagServerIP, err)
}
if err := kv.Set("www.camlistore.net.", *flagServerIP); err != nil {
log.Fatalf("Error adding %v:%v record: %v", "www.camlistore.net.", *flagServerIP, err)
}
lastCamwebUpdate = time.Now()
if stagingIP, err := stagingCamwebIP(); err == nil {
if err := kv.Set(stagingCamwebHost+".", stagingIP); err != nil {
log.Fatalf("Error adding %v:%v record: %v", stagingCamwebHost+".", stagingIP, err)
}
}
ds := newDNSServer(kv)
cs := &gpgchallenge.Server{
OnSuccess: func(identity string, address string) error {
log.Printf("Adding %v.camlistore.net. as %v", identity, address)
return ds.dataSource.Set(strings.ToLower(identity+".camlistore.net."), address)
},
}
tcperr := make(chan error, 1)
udperr := make(chan error, 1)
httpserr := make(chan error, 1)
httperr := make(chan error, 1)
log.Printf("serving DNS on %s\n", *addr)
go func() {
tcperr <- dns.ListenAndServe(*addr, "tcp", ds)
}()
go func() {
udperr <- dns.ListenAndServe(*addr, "udp", ds)
}()
if metadata.OnGCE() {
// TODO(mpl): if we want to get a cert for anything
// *.camlistore.net, it's a bit of a chicken and egg problem, since
// we need camnetdns itself to be already running and answering DNS
// queries. It's probably doable, but easier for now to just ask
// one for camnetdns.camlistore.org, since that name is not
// resolved by camnetdns.
hostname := strings.TrimSuffix(authorityNS, ".")
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(hostname),
Cache: autocert.DirCache(osutil.DefaultLetsEncryptCache()),
}
go func() {
httperr <- http.ListenAndServe(":http", m.HTTPHandler(nil))
}()
ln, err := tls.Listen("tcp", httpsListenAddr, &tls.Config{
Rand: rand.Reader,
Time: time.Now,
NextProtos: []string{http2.NextProtoTLS, "http/1.1"},
MinVersion: tls.VersionTLS12,
GetCertificate: m.GetCertificate,
})
if err != nil {
log.Fatalf("Error listening on %v: %v", httpsListenAddr, err)
}
go func() {
httpserr <- http.Serve(ln, cs)
}()
}
select {
case err := <-tcperr:
log.Fatalf("DNS over TCP error: %v", err)
case err := <-udperr:
log.Fatalf("DNS error: %v", err)
case err := <-httpserr:
log.Fatalf("HTTPS server error: %v", err)
case err := <-httperr:
log.Fatalf("HTTP server for Let's Encrypt error: %v", err)
}
}

View File

@ -1,159 +0,0 @@
/*
Copyright 2018 The Perkeep Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code related to obtaining camlistore.net DNS subdomains.
package main
import (
"crypto/tls"
"errors"
"fmt"
"log"
"net"
"net/http"
"strings"
"time"
"golang.org/x/crypto/acme/autocert"
"perkeep.org/internal/httputil"
"perkeep.org/internal/osutil"
"perkeep.org/internal/osutil/gce"
"perkeep.org/pkg/env"
"perkeep.org/pkg/gpgchallenge"
"perkeep.org/pkg/serverinit"
"perkeep.org/pkg/webserver"
)
// For getting a name in camlistore.net
const (
camliNetDNS = serverinit.CamliNetDNS
camliNetDomain = serverinit.CamliNetDomain
)
var camliNetHostName string // <keyId>.camlistore.net
// listenForCamliNet prepares the TLS listener for both the GPG challenge, and
// for Let's Encrypt. It then starts listening and returns the baseURL derived from
// the hostname we should obtain from the GPG challenge.
func listenForCamliNet(ws *webserver.Server, config *serverinit.Config) (baseURL string, err error) {
camliNetIP := config.CamliNetIP()
if camliNetIP == "" {
return "", errors.New("no camliNetIP")
}
if ip := net.ParseIP(camliNetIP); ip == nil {
return "", fmt.Errorf("camliNetIP value %q is not a valid IP address", camliNetIP)
} else if ip.To4() == nil {
// TODO: support IPv6 when GCE supports IPv6: https://code.google.com/p/google-compute-engine/issues/detail?id=8
return "", errors.New("CamliNetIP should be an IPv4, as IPv6 is not yet supported on GCE")
}
challengeHostname := camliNetIP + gpgchallenge.SNISuffix
selfCert, selfKey, err := httputil.GenSelfTLS(challengeHostname)
if err != nil {
return "", fmt.Errorf("could not generate self-signed certificate: %v", err)
}
gpgchallengeCert, err := tls.X509KeyPair(selfCert, selfKey)
if err != nil {
return "", fmt.Errorf("could not load TLS certificate: %v", err)
}
_, keyId, err := config.KeyRingAndId()
if err != nil {
return "", fmt.Errorf("could not get keyId for camliNet hostname: %v", err)
}
// catch future length changes
if len(keyId) != 16 {
panic("length of GPG keyId is not 16 anymore")
}
shortKeyId := keyId[8:]
camliNetHostName = strings.ToLower(shortKeyId + "." + camliNetDomain)
m := autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(camliNetHostName),
Cache: autocert.DirCache(osutil.DefaultLetsEncryptCache()),
}
go func() {
err := http.ListenAndServe(":http", m.HTTPHandler(nil))
log.Fatalf("Could not start server for http-01 challenge: %v", err)
}()
getCertificate := func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hello.ServerName == challengeHostname {
return &gpgchallengeCert, nil
}
return m.GetCertificate(hello)
}
log.Printf("TLS enabled, with Let's Encrypt for %v", camliNetHostName)
ws.SetTLS(webserver.TLSSetup{CertManager: getCertificate})
err = ws.Listen(fmt.Sprintf(":%d", gpgchallenge.ClientChallengedPort))
if err != nil {
return "", fmt.Errorf("Listen: %v", err)
}
return fmt.Sprintf("https://%s", camliNetHostName), nil
}
// registerDNSChallengeHandler initializes and returns the
// gpgchallenge Client if camliNetIP is configured and if so,
// registers its handler with Perkeep's muxer.
//
// If camlistore.net support isn't enabled, it returns (nil, nil).
func registerDNSChallengeHandler(ws *webserver.Server, config *serverinit.Config) (*gpgchallenge.Client, error) {
camliNetIP := config.CamliNetIP()
if camliNetIP == "" {
return nil, nil
}
if ip := net.ParseIP(camliNetIP); ip == nil {
return nil, fmt.Errorf("camliNetIP value %q is not a valid IP address", camliNetIP)
}
keyRing, keyId, err := config.KeyRingAndId()
if err != nil {
return nil, err
}
cl, err := gpgchallenge.NewClient(keyRing, keyId, camliNetIP)
if err != nil {
return nil, fmt.Errorf("could not init gpgchallenge client: %v", err)
}
ws.Handle(cl.Handler())
return cl, nil
}
// requestHostName performs the GPG challenge to register/obtain a name in the
// camlistore.net domain. The acquired name should be "<gpgKeyId>.camlistore.net",
// where <gpgKeyId> is the short form (8 trailing chars) of Perkeep's keyId.
// It also starts a goroutine that will rerun the challenge every hour, to keep
// the camlistore.net DNS server up to date.
func requestHostName(cl *gpgchallenge.Client) error {
if err := cl.Challenge(camliNetDNS); err != nil {
return err
}
if env.OnGCE() {
if err := gce.SetInstanceHostname(camliNetHostName); err != nil {
return fmt.Errorf("error setting instance camlistore-hostname: %v", err)
}
}
var repeatChallengeFn func()
repeatChallengeFn = func() {
if err := cl.Challenge(camliNetDNS); err != nil {
log.Printf("error with hourly DNS challenge: %v", err)
}
time.AfterFunc(time.Hour, repeatChallengeFn)
}
time.AfterFunc(time.Hour, repeatChallengeFn)
return nil
}

View File

@ -302,15 +302,7 @@ func handleSignals(shutdownc <-chan io.Closer) {
// listen discovers the listen address, base URL, and hostname that the ws is
// going to use, sets up the TLS configuration, and starts listening.
//
// If camliNetIP is configured, it also prepares for the GPG
// challenge, to register/acquire a name in the camlistore.net domain.
func listen(ws *webserver.Server, config *serverinit.Config) (baseURL string, err error) {
camliNetIP := config.CamliNetIP()
if camliNetIP != "" {
return listenForCamliNet(ws, config)
}
baseURL = config.BaseURL()
// Prefer the --listen flag value. Otherwise use the config value.
@ -435,11 +427,6 @@ func main() {
exitf("Error starting webserver: %v", err)
}
challengeClient, err := registerDNSChallengeHandler(ws, config)
if err != nil {
exitf("Error registering challenge client with Perkeep muxer: %v", err)
}
config.SetReindex(*flagReindex)
config.SetKeepGoing(*flagKeepGoing)
@ -452,11 +439,6 @@ func main() {
go ws.Serve()
if challengeClient != nil {
if err := requestHostName(challengeClient); err != nil {
exitf("Could not register on camlistore.net: %v", err)
}
}
if env.OnGCE() {
gce.FixUserDataForPerkeepRename()
}