Skip to content

Instantly share code, notes, and snippets.

@ngerakines
Created January 26, 2025 16:04
Show Gist options
  • Save ngerakines/9aa381011322bd57bd978b279ef8cc76 to your computer and use it in GitHub Desktop.
Save ngerakines/9aa381011322bd57bd978b279ef8cc76 to your computer and use it in GitHub Desktop.

DNS

Build

  1. Build CoreDNS with the records plugin. This isn't super straight forward.

  2. Build the magicdns-admin container

Startup

  1. Start tailscale

  2. Create SSL cert

  3. Start nginx

  4. Start app

  5. Start coredns

version: '3.8'
volumes:
dns_db:
dns_ts:
dns_tls:
dns_coredns:
services:
coredns:
image: coredns
network_mode: service:tailscale
restart: on-failure
volumes:
- dns_coredns:/etc/coredns/
entrypoint: /coredns
command: -conf /etc/coredns/Corefile
app:
image: "magicdns-admin"
restart: unless-stopped
environment:
- PDS_ADMIN_PASSWORD=secret
- DATABASE=/etc/coredns/database.db
- COREFILE=/etc/coredns/Corefile
volumes:
- dns_coredns:/etc/coredns/
tailscale:
image: tailscale/tailscale:latest
restart: unless-stopped
environment:
- TS_AUTHKEY=tskey-auth-your-secret-goes-here
- TS_STATE_DIR=/var/run/tailscale
- TS_EXTRA_ARGS=--advertise-tags=tag:pdsdns
- TS_HOSTNAME=pdsdns
volumes:
- dns_tls:/mnt/tls
- dns_ts:/var/run/tailscale
nginx:
image: nginx
restart: unless-stopped
network_mode: service:tailscale
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- dns_tls:/mnt/tls:ro
FROM golang:alpine3.21 AS build
ENV CGO_ENABLED=1
RUN apk add --no-cache gcc musl-dev
WORKDIR /workspace
COPY go.mod /workspace/
COPY go.sum /workspace/
RUN go mod download
COPY main.go /workspace/
ENV GOCACHE=/root/.cache/go-build
RUN --mount=type=cache,target="/root/.cache/go-build" go install -ldflags='-s -w -extldflags "-static"' ./main.go
FROM scratch
COPY --from=build /go/bin/main /usr/local/bin/magicdev-admin
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT [ "/usr/local/bin/magicdev-admin" ]
module github.com/astrenoxcoop/magicdev-admin
go 1.23.5
require (
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/mattn/go-sqlite3 v1.14.24 // indirect
github.com/sethvargo/go-envconfig v1.1.0 // indirect
go.etcd.io/etcd/api/v3 v3.5.17 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.17 // indirect
go.etcd.io/etcd/client/v3 v3.5.17 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.17.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
)
package main
import (
"bytes"
"context"
"database/sql"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"text/template"
petname "github.com/dustinkirkland/golang-petname"
_ "github.com/mattn/go-sqlite3"
"github.com/sethvargo/go-envconfig"
)
type ServerConfig struct {
Database string `env:"DATABASE, default=./database.db"`
PDSHostName string `env:"PDS_HOSTNAME, default=pds.my.ts.net"`
PDSAdminPassword string `env:"PDS_ADMIN_PASSWORD, required"`
Domain string `env:"DOMAIN, default=pyroclastic.cloud"`
Corefile string `env:"COREFILE, default=./Corefile"`
}
type createInviteResponse struct {
Code string `json:"code"`
}
type createAccountRequest struct {
Email string `json:"email"`
Handle string `json:"handle"`
Password string `json:"password"`
InviteCode string `json:"inviteCode"`
}
type createAccountResponse struct {
DID string `json:"did"`
}
func createInvite(server, password string) (string, error) {
url := fmt.Sprintf("https://%s/xrpc/com.atproto.server.createInviteCode", server)
requestBody := []byte(`{"useCount":1}`)
req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:"+password)))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
decoder := json.NewDecoder(resp.Body)
var createInvite createInviteResponse
err = decoder.Decode(&createInvite)
if err != nil {
return "", err
}
return createInvite.Code, nil
}
func createAccount(server, password, inviteCode, handle, email string) (string, error) {
url := fmt.Sprintf("https://%s/xrpc/com.atproto.server.createAccount", server)
createAccountRequestBody := createAccountRequest{
Email: email,
Handle: handle,
Password: "password",
InviteCode: inviteCode,
}
requestBody, err := json.Marshal(createAccountRequestBody)
if err != nil {
return "", err
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:"+password)))
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
decoder := json.NewDecoder(resp.Body)
var createdAccount createAccountResponse
err = decoder.Decode(&createdAccount)
if err != nil {
return "", err
}
return createdAccount.DID, nil
}
type handlers struct {
config *ServerConfig
db *sql.DB
}
func (h *handlers) indexHandler(w http.ResponseWriter, r *http.Request) {
handle := r.PostFormValue("handle")
if handle == "" {
handle = petname.Generate(2, "-")
body := fmt.Sprintf(`<html><body><form method="post" action="/"><input type="text" name="handle" value="%s" /><input type="submit" /></form></body></html>`, handle)
io.WriteString(w, body)
return
}
inviteCode, err := createInvite(h.config.PDSHostName, h.config.PDSAdminPassword)
if err != nil {
io.WriteString(w, fmt.Sprintf("Error: %s", err))
return
}
email := fmt.Sprintf("%s@%s", handle, h.config.Domain)
full_handle := fmt.Sprintf("%s.%s", handle, h.config.Domain)
did, err := createAccount(h.config.PDSHostName, h.config.PDSAdminPassword, inviteCode, full_handle, email)
if err != nil {
io.WriteString(w, fmt.Sprintf("Error: %s", err))
return
}
_, err = h.db.Exec(`INSERT INTO handles (did, handle) VALUES (?, ?)`, &did, &handle)
if err != nil {
io.WriteString(w, fmt.Sprintf("Error: %s", err))
return
}
if err = generateCorefile(h.config, h.db); err != nil {
io.WriteString(w, fmt.Sprintf("Error: %s", err))
return
}
body := fmt.Sprintf(`<html><body><p>Created <span>%s</span> with handle <span>%s</span></p><p><a href="/">Back</a></body></html>`, did, full_handle)
io.WriteString(w, body)
}
func (h *handlers) newHandler(w http.ResponseWriter, r *http.Request) {
handle := r.PostFormValue("handle")
if handle == "" {
handle = "testy"
}
io.WriteString(w, fmt.Sprintf("Hello, %s!\n", handle))
}
func generateCorefile(config *ServerConfig, db *sql.DB) error {
corefileTemplate := `
. {
log
errors
reload 10s
records {{ .Domain }} {
@ 60 IN TXT "TEST"
{{ range .Records }}
_atproto.{{ .Handle }} 60 IN TXT "did={{ .DID }}"{{ end }}
}
}`
corefile, err := template.New("corefile").Parse(corefileTemplate)
if err != nil {
log.Fatal(err)
}
type corefileValueRecord struct {
DID string
Handle string
}
type corefileValues struct {
Domain string
Records []corefileValueRecord
}
records := make([]corefileValueRecord, 0)
rows, err := db.Query("SELECT handle, did FROM handles")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var handle string
var did string
err = rows.Scan(&handle, &did)
if err != nil {
return err
}
records = append(records, corefileValueRecord{did, handle})
}
err = rows.Err()
if err != nil {
return err
}
data := corefileValues{
Domain: config.Domain,
Records: records,
}
output, err := os.Create(config.Corefile)
if err != nil {
return err
}
defer output.Close()
err = corefile.Execute(output, data)
if err != nil {
log.Fatal(err)
}
return nil
}
func main() {
ctx := context.Background()
var config ServerConfig
if err := envconfig.Process(ctx, &config); err != nil {
log.Fatal(err)
}
db, err := sql.Open("sqlite3", config.Database)
if err != nil {
log.Fatal(err)
}
defer db.Close()
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS handles (did TEXT NOT NULL PRIMARY KEY, handle TEXT NOT NULL UNIQUE)`)
if err != nil {
log.Fatal(err)
}
h := handlers{
config: &config,
db: db,
}
mux := http.NewServeMux()
mux.HandleFunc("/", h.indexHandler)
mux.HandleFunc("/new", h.newHandler)
if err = http.ListenAndServe(":3333", mux); !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}
events {}
http {
server {
resolver 127.0.0.11 [::1]:5353 valid=15s;
set $backend "http://app:3333";
listen 443 ssl;
ssl_certificate /mnt/tls/cert.pem;
ssl_certificate_key /mnt/tls/cert.key;
location / {
proxy_pass $backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 64M;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment