Skip to content

Commit

Permalink
feat: initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
aschmahmann committed Aug 20, 2024
1 parent 75c2c68 commit 83323ca
Show file tree
Hide file tree
Showing 10 changed files with 2,251 additions and 0 deletions.
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,80 @@
# p2p-forge

> An Authoritative DNS server for distributing DNS subdomains to libp2p peers
## Build

`go build` will build the binary in your local directory

## Install

```console
$ go install github.com/ipshipyard/p2p-forge@latest
```

Will download using go mod, build and install the binary in your global Go binary directory (e.g. `~/go/bin`)

### From source
`go install` will build and install the binary in your global Go binary directory (e.g. `~/go/bin`)

## Usage

### Handled DNS records

There are 3 types of records handled for a given peer and forge (e.g. `<peerID>.libp2p.direct`):
- ACME Challenges for a given peerID `_acme-challenge.<peerID>.libp2p.direct`
- A records for an IPv4 prefixed subdomain like `1-2-3-4.<peerID>.libp2p.direct`
- AAAA records for an IPv6 prefixed subdomain like `2001-db8--.<peerID>.libp2p.direct`

#### IPv4 subdomain handling

IPv4 handling is fairly straightforward, for a given IPv4 address `1.2.3.4` convert the `.`s into `-`s and the result
will be valid.

#### IPv6 subdomain handling

Due to the length of IPv6 addresses there are a number of different formats for describing IPv6 addresses.

The addresses handled here are:
- For an address `A:B:C:D:1:2:3:4` convert the `:`s into `-`s and the result will be valid.
- Addresses of the form `A::C:D` can be converted either into their expanded form or into a condensed form by replacing
the `:`s with `-`s, like `A--C-D`
- When there is a `:` as the first or last character it must be converted to a 0 to comply with [rfc1123](https://datatracker.ietf.org/doc/html/rfc1123#section-2)
, so `::B:C:D` would become `0--B-C-D` and `1::` would become `1--0`

Other address formats (e.g. the dual IPv6/IPv4 format) are not supported

### Submitting Challenge Records

To claim a domain name like `<peerID>.libp2p.direct` requires:
1. The private key corresponding to the given peerID
2. A publicly reachable libp2p endpoint with one of the following libp2p transport configurations:
- QUIC-v1
- TCP or WS or WSS, Yamux, TLS or Noise
- WebTransport
- Note: The [Identify protocol](https://github.com/libp2p/specs/tree/master/identify) (`/ipfs/id/1.0.0`)
- Other transports are under consideration (e.g. HTTP), if they are of interest please file an issue

To set an ACME challenge send an HTTP request to the server (for libp2p.direct this is registration.libp2p.direct)
```shell
curl -X POST "https://registration.libp2p.direct/v1/<peerID>/_acme-challenge" \
-H "Authorization: Bearer <signature>.<public_key>"
-H "Content-Type: application/json" \
-d '{
"value": "your_acme_challenge_token",
"addresses": "[your_multiaddrs, comma_separated]"
}'
```

Where the signature is a base64 encoding of the signature for a [libp2p signed envelope](https://github.com/libp2p/specs/blob/master/RFC/0002-signed-envelopes.md)
where:
- The domain separation string is "peer-forge-domain-challenge"
- The payload type is the ASCII string "/peer-forge-domain-challenge"
- The payload bytes are the contents of the body of the request

If the public key is not extractable from the peerID then after the signature add a `.` followed by the base64 encoded
public key in the libp2p public key format.

Note: Per the [peerID spec](https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#peer-ids) the peerIDs with
extractable public keys are those that are encoded as fewer than 42 bytes (i.e. Ed25519 and Secp256k1), which means the
others (i.e. RSA and ECDSA) require the public keys to be in the Authorization header.
82 changes: 82 additions & 0 deletions acme/reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package acme

import (
"context"
"strings"
"time"

"github.com/coredns/coredns/plugin"
"github.com/ipfs/go-datastore"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/miekg/dns"
)

type acmeReader struct {
Next plugin.Handler
ForgeDomain string
Datastore datastore.Datastore
}

const ttl = 1 * time.Hour

// ServeDNS implements the plugin.Handler interface.
func (p acmeReader) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
var answers []dns.RR
for _, q := range r.Question {
if q.Qtype != dns.TypeTXT && q.Qtype != dns.TypeANY {
continue
}

subdomain := strings.TrimSuffix(q.Name, "."+p.ForgeDomain+".")
if len(subdomain) == len(q.Name) || len(subdomain) == 0 {
continue
}

domainSegments := strings.Split(subdomain, ".")
if len(domainSegments) != 2 {
continue
}

peerIDStr := domainSegments[1]
peerID, err := peer.Decode(peerIDStr)
if err != nil {
continue
}

const acmeSubdomain = "_acme-challenge"
prefix := domainSegments[0]
if prefix != acmeSubdomain {
continue
}

val, err := p.Datastore.Get(ctx, datastore.NewKey(peerID.String()))
if err != nil {
continue
}

answers = append(answers, &dns.TXT{
Hdr: dns.RR_Header{
Name: dns.Fqdn(q.Name),
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: uint32(ttl.Seconds()),
},
Txt: []string{string(val)},
})
}

if len(answers) > 0 {
var m dns.Msg
m.SetReply(r)
m.Authoritative = true
m.Answer = answers
w.WriteMsg(&m)
return dns.RcodeSuccess, nil
}

// Call next plugin (if any).
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
}

// Name implements the Handler interface.
func (p acmeReader) Name() string { return pluginName }
87 changes: 87 additions & 0 deletions acme/setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package acme

import (
"fmt"
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/ipfs/go-datastore"

badger4 "github.com/ipfs/go-ds-badger4"

"github.com/aws/aws-sdk-go/aws/session"
ddbv1 "github.com/aws/aws-sdk-go/service/dynamodb"
ddbds "github.com/ipfs/go-ds-dynamodb"
)

const pluginName = "acme"

func init() { plugin.Register(pluginName, setup) }

func setup(c *caddy.Controller) error {
reader, writer, err := parse(c)
if err != nil {
return plugin.Error(pluginName, err)
}

c.OnStartup(writer.OnStartup)
c.OnRestart(writer.OnReload)
c.OnFinalShutdown(writer.OnFinalShutdown)
c.OnRestartFailed(writer.OnStartup)

// Add the read portion of the plugin to CoreDNS, so Servers can use it in their plugin chain.
// The write portion is not *really* a plugin just a separate webserver running.
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
return reader
})

return nil
}

func parse(c *caddy.Controller) (*acmeReader, *acmeWriter, error) {
var forgeDomain string
var httpListenAddr string
var databaseType string

// Parse the configuration from the Corefile
c.Next()
args := c.RemainingArgs()
if len(args) < 3 {
return nil, nil, fmt.Errorf("invalid arguments")
}

forgeDomain = args[0]
httpListenAddr = args[1]
databaseType = args[2]

var ds datastore.TTLDatastore

switch databaseType {
case "dynamo":
ddbClient := ddbv1.New(session.Must(session.NewSession()))
ds = ddbds.New(ddbClient, "foo")
case "badger":
if len(args) != 4 {
return nil, nil, fmt.Errorf("need to pass a path for the Badger configuration")
}
dbPath := args[3]
var err error
ds, err = badger4.NewDatastore(dbPath, nil)
if err != nil {
return nil, nil, err
}
default:
return nil, nil, fmt.Errorf("unknown database type: %s", databaseType)
}

writer := &acmeWriter{
Addr: httpListenAddr,
Datastore: ds,
}
reader := &acmeReader{
ForgeDomain: forgeDomain,
Datastore: ds,
}

return reader, writer, nil
}
Loading

0 comments on commit 83323ca

Please sign in to comment.