-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
75c2c68
commit 83323ca
Showing
10 changed files
with
2,251 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.