WIP add server cmd

Kim committed Oct 28, 2025 at 10:09 UTC 28a19c20dcc26077be4b7862dbd9377cd390b3a2
4 files changed +236 -1
cmd/server/main.go new
+69
@@ -0,0 +1,69 @@
1 +package main
2 +
3 +import (
4 + "context"
5 + "fmt"
6 + "os/signal"
7 + "syscall"
8 + "time"
9 +
10 + "github.com/rs/zerolog/log"
11 + "github.com/spf13/cobra"
12 +
13 + "github.com/gosuda/relaydns/relaydns"
14 + "github.com/gosuda/relaydns/relaydns/core/cryptoops"
15 +)
16 +
17 +var rootCmd = &cobra.Command{
18 + Use: "relayserver",
19 + Short: "A lightweight, DNS-driven peer-to-peer proxy",
20 + RunE: runServer,
21 +}
22 +
23 +var (
24 + flagPort int // admin UI + HTTP proxy port (e.g. 4017)
25 + bootstraps []string
26 +)
27 +
28 +func init() {
29 + flags := rootCmd.PersistentFlags()
30 + flags.IntVar(&flagPort, "port", 4017, "admin UI and HTTP proxy port")
31 + flags.StringArrayVar(&bootstraps, "bootstraps", nil, "bootstrap addresses")
32 +}
33 +
34 +func main() {
35 + if err := rootCmd.Execute(); err != nil {
36 + log.Fatal().Err(err).Msg("execute root command")
37 + }
38 +}
39 +
40 +func runServer(cmd *cobra.Command, args []string) error {
41 + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
42 + defer stop()
43 +
44 + cred, err := cryptoops.NewCredential()
45 + if err != nil {
46 + return err
47 + }
48 +
49 + serv := relaydns.NewRelayServer(cred, bootstraps)
50 + serv.Start()
51 + defer serv.Stop()
52 +
53 + // Admin UI + per-peer HTTP proxy
54 + httpSrv := serveHTTP(ctx, fmt.Sprintf(":%d", flagPort), serv, stop)
55 +
56 + <-ctx.Done()
57 + log.Info().Msg("[server] shutting down...")
58 +
59 + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
60 + defer cancel()
61 + if httpSrv != nil {
62 + if err := httpSrv.Shutdown(shutdownCtx); err != nil {
63 + log.Error().Err(err).Msg("[server] http server shutdown error")
64 + }
65 + }
66 +
67 + log.Info().Msg("[server] shutdown complete")
68 + return nil
69 +}
cmd/server/view.go new
+134
@@ -0,0 +1,134 @@
1 +package main
2 +
3 +import (
4 + "context"
5 + "encoding/json"
6 + "html/template"
7 + "net/http"
8 +
9 + "github.com/rs/zerolog/log"
10 +
11 + "github.com/gosuda/relaydns/relaydns"
12 +)
13 +
14 +// serveHTTP builds the HTTP mux and returns the server.
15 +func serveHTTP(ctx context.Context, addr string, serv *relaydns.RelayServer, cancel context.CancelFunc) *http.Server {
16 + if addr == "" {
17 + addr = ":0"
18 + }
19 +
20 + mux := http.NewServeMux()
21 +
22 + // Index page
23 + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
24 + if r.URL.Path != "/" {
25 + http.NotFound(w, r)
26 + return
27 + }
28 +
29 + w.Header().Set("Content-Type", "text/html; charset=utf-8")
30 + log.Debug().Msg("render admin index")
31 + _ = serverTmpl.Execute(w, nil) // TODO
32 + })
33 +
34 + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
35 + type info struct {
36 + Status string `json:"status"`
37 + }
38 + resp := info{Status: "ok"}
39 + w.Header().Set("Content-Type", "application/json")
40 + _ = json.NewEncoder(w).Encode(resp)
41 + })
42 +
43 + srv := &http.Server{
44 + Addr: addr,
45 + Handler: mux,
46 + }
47 +
48 + go func() {
49 + log.Info().Msgf("[server] http: %s", addr)
50 + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
51 + log.Error().Err(err).Msg("[server] http error")
52 + cancel()
53 + }
54 + }()
55 +
56 + return srv
57 +}
58 +
59 +var serverTmpl = template.Must(template.New("admin-index").Parse(`<!doctype html>
60 +<html lang="ko">
61 +<head>
62 + <meta charset="utf-8"/>
63 + <meta name="viewport" content="width=device-width, initial-scale=1" />
64 + <title>RelayDNS — Admin</title>
65 + <style>
66 + * { box-sizing: border-box }
67 + :root {
68 + --bg:#fafbff; --panel:#ffffff; --ink:#0f172a; --muted:#6b7280; --line:#e9eef5;
69 + --primary:#2563eb; --ok:#059669; --bad:#b91c1c; --ok-bg:#ecfdf5; --bad-bg:#fee2e2;
70 + }
71 + body { margin:0; background:var(--bg); color:var(--ink); font-family:sans-serif; font-size:16px; line-height:1.6 }
72 + .wrap { max-width: 980px; margin: 0 auto; padding: 32px 20px }
73 + header { display:flex; align-items:center; justify-content:space-between; padding: 20px 24px; background:var(--panel); border:1px solid var(--line); border-radius: 14px }
74 + .brand { font-weight:800; font-size:22px; letter-spacing:.2px }
75 + .status { color:var(--ok); font-weight:700 }
76 + main { margin-top: 22px }
77 + .section { background:var(--panel); border:1px solid var(--line); border-radius:14px; padding:18px; margin-bottom:14px }
78 + .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; color:#374151; word-break: break-all }
79 + .title { font-weight:800; margin:0 0 10px 0; font-size:18px }
80 + .muted { color:var(--muted); font-size:14px }
81 + .pill { display:inline-flex; align-items:center; gap:8px; padding:6px 10px; border-radius:999px; font-weight:800; font-size:13px }
82 + .pill.ok { background:var(--ok-bg); color:var(--ok) }
83 + .pill.bad { background:var(--bad-bg); color:var(--bad) }
84 + .pill .dot { width:8px; height:8px; border-radius:999px; background:var(--ok); display:inline-block }
85 + .pill.bad .dot { background:var(--bad) }
86 + .head { display:flex; align-items:center; justify-content:space-between; gap:12px }
87 + .btn { display:inline-block; background:var(--primary); color:#fff; text-decoration:none; border-radius:10px; padding:10px 14px; font-weight:800; margin-top:8px }
88 + </style>
89 + </head>
90 +<body>
91 + <div class="wrap">
92 + <header>
93 + <div class="brand">RelayDNS</div>
94 + <div class="status">Admin</div>
95 + </header>
96 + <main>
97 + <section class="section">
98 + <div class="title">Server</div>
99 + <div class="mono">Peer ID: {{.NodeID}}</div>
100 + {{if .Addrs}}
101 + <div class="muted" style="margin-top:6px">Multiaddrs</div>
102 + <div class="mono">{{range .Addrs}}{{.}}<br/>{{end}}</div>
103 + {{end}}
104 + <div class="muted" style="margin-top:6px">Known clients: {{len .Rows}}</div>
105 + </section>
106 + {{range .Rows}}
107 + <section class="section" id="peer-{{.Peer}}" data-peer="{{.Peer}}" data-name="{{.Name}}">
108 + <div class="head">
109 + <div class="title">{{if .Name}}{{.Name}}{{else}}(unnamed){{end}}</div>
110 + <div>
111 + <span class="muted" style="margin-right:8px">{{.Kind}}</span>
112 + {{if .Connected}}
113 + <span class="pill ok"><span class="dot"></span>Connected</span>
114 + {{else}}
115 + <span class="pill bad"><span class="dot"></span>Disconnected</span>
116 + {{end}}
117 + </div>
118 + </div>
119 + {{if .DNS}}<div class="muted">DNS: <span class="mono">{{.DNS}}</span></div>{{end}}
120 + <div class="muted">Peer</div>
121 + <div class="mono">{{.Peer}}</div>
122 + <div class="muted" style="margin-top:6px">Last seen: {{.LastSeen}}{{if .TTL}} · TTL: {{.TTL}}{{end}}</div>
123 + <a class="btn" href="{{.Link}}">Open</a>
124 + </section>
125 + {{else}}
126 + <section class="section">
127 + <div class="title">No clients discovered</div>
128 + <div class="muted">Start a client and ensure bootstraps are configured.</div>
129 + </section>
130 + {{end}}
131 + </main>
132 + </div>
133 +</body>
134 +</html>`))
go.mod
+9 -1
@@ -6,9 +6,17 @@ require (
6 github.com/gorilla/websocket v1.5.3
7 github.com/hashicorp/yamux v0.1.2
8 github.com/planetscale/vtprotobuf v0.6.0
9 + github.com/rs/zerolog v1.34.0
10 + github.com/spf13/cobra v1.10.1
11 github.com/valyala/bytebufferpool v1.0.0
12 golang.org/x/crypto v0.43.0
13 google.golang.org/protobuf v1.36.10
14 )
15
14 -require golang.org/x/sys v0.37.0 // indirect
16 +require (
17 + github.com/inconshreveable/mousetrap v1.1.0 // indirect
18 + github.com/mattn/go-colorable v0.1.13 // indirect
19 + github.com/mattn/go-isatty v0.0.19 // indirect
20 + github.com/spf13/pflag v1.0.9 // indirect
21 + golang.org/x/sys v0.37.0 // indirect
22 +)
go.sum
+24
@@ -1,16 +1,40 @@
1 +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
2 +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
3 +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
4 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
5 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
6 github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
7 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
8 github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
9 github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
10 +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
11 +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
12 +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
13 +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
14 +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
15 +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
16 +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
17 +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
18 github.com/planetscale/vtprotobuf v0.6.0 h1:nBeETjudeJ5ZgBHUz1fVHvbqUKnYOXNhsIEabROxmNA=
19 github.com/planetscale/vtprotobuf v0.6.0/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
20 +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
21 +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
22 +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
23 +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
24 +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
25 +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
26 +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
27 +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
28 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
29 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
30 golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
31 golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
32 +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
33 +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
34 +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
35 golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
36 golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
37 google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
38 google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
39 +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
40 +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=