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=