add example client and server
Kim committed
Oct 21, 2025 at 11:37 UTC
831c6330fdeea4b80d30b8a3afdb71480a945c5f
6 files changed
+397
-84
cmd/example_client/main.go
new
+158
@@ -0,0 +1,158 @@
1
+package main
2
+
3
+import (
4
+ "context"
5
+ "fmt"
6
+ "log"
7
+ "net/http"
8
+ "os"
9
+ "os/signal"
10
+ "syscall"
11
+ "text/template"
12
+ "time"
13
+
14
+ "github.com/gosuda/relaydns/relaydns"
15
+ "github.com/spf13/cobra"
16
+)
17
+
18
+var rootCmd = &cobra.Command{
19
+ Use: "relaydns-client",
20
+ Short: "RelayDNS demo client (local HTTP backend + libp2p advertiser)",
21
+ RunE: runClient,
22
+}
23
+
24
+var (
25
+ flagServerURL string
26
+ flagBootstraps []string
27
+ flagRelay bool
28
+ flagBackendHTTP string
29
+ flagProtocol string
30
+ flagTopic string
31
+ flagAdvertiseEvery time.Duration
32
+ flagName string
33
+ flagDNS string
34
+)
35
+
36
+func init() {
37
+ flags := rootCmd.PersistentFlags()
38
+ flags.StringVar(&flagServerURL, "server-url", "http://localhost:8080", "relayserver admin base URL (e.g. http://127.0.0.1:9090) to auto-fetch multiaddrs from /health")
39
+ flags.StringSliceVar(&flagBootstraps, "bootstrap", nil, "multiaddrs with /p2p/ (supports /dnsaddr/ that resolves to /p2p/)")
40
+ flags.BoolVar(&flagRelay, "relay", true, "enable libp2p relay/hole-punch support")
41
+ flags.StringVar(&flagBackendHTTP, "backend-http", ":8081", "local backend HTTP listen address")
42
+ flags.StringVar(&flagProtocol, "protocol", "/relaydns/http/1.0", "libp2p protocol id for streams (must match server)")
43
+ flags.StringVar(&flagTopic, "topic", "relaydns.backends", "pubsub topic for backend adverts")
44
+ flags.DurationVar(&flagAdvertiseEvery, "advertise-every", 3*time.Second, "interval for backend adverts")
45
+ flags.StringVar(&flagName, "name", "demo-http", "backend display name")
46
+ flags.StringVar(&flagDNS, "dns", "demo-http.example", "backend DNS metadata (optional)")
47
+}
48
+
49
+func main() {
50
+ if err := rootCmd.Execute(); err != nil {
51
+ log.Fatal(err)
52
+ }
53
+}
54
+
55
+func runClient(cmd *cobra.Command, args []string) error {
56
+ ctx, cancel := context.WithCancel(context.Background())
57
+ defer cancel()
58
+
59
+ // 1) 로컬 HTTP 백엔드
60
+ go func() {
61
+ mux := http.NewServeMux()
62
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
63
+ data := struct {
64
+ Now string
65
+ Host string
66
+ Addr string
67
+ }{
68
+ Now: time.Now().Format(time.RFC1123),
69
+ Host: r.Host,
70
+ Addr: flagBackendHTTP,
71
+ }
72
+ _ = pageTmpl.Execute(w, data)
73
+ })
74
+ mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
75
+ w.WriteHeader(http.StatusOK)
76
+ _, _ = w.Write([]byte("ok"))
77
+ })
78
+
79
+ log.Println("[client] local backend http", flagBackendHTTP)
80
+ if err := http.ListenAndServe(flagBackendHTTP, mux); err != nil {
81
+ log.Println("[client] http backend error:", err)
82
+ cancel()
83
+ }
84
+ }()
85
+
86
+ // 2) libp2p host
87
+ h, err := relaydns.MakeHost(ctx, 0, flagRelay)
88
+ if err != nil {
89
+ return fmt.Errorf("make host: %w", err)
90
+ }
91
+
92
+ client, err := relaydns.NewClient(ctx, h, relaydns.ClientConfig{
93
+ Protocol: "/relaydns/http/1.0",
94
+ Topic: "relaydns.backends",
95
+ AdvertiseEvery: 3 * time.Second,
96
+ Name: "demo-http",
97
+ DNS: "demo-http.example",
98
+ TargetTCP: "127.0.0.1:8081",
99
+
100
+ // 편의성 업!
101
+ ServerURL: flagServerURL,
102
+ Bootstraps: flagBootstraps,
103
+ PreferQUIC: true,
104
+ PreferLocal: true,
105
+ })
106
+ if err != nil {
107
+ return fmt.Errorf("new client: %w", err)
108
+ }
109
+ defer client.Close()
110
+
111
+ // 자기 주소/피어ID 로그로 찍어서 디버깅 편하게
112
+ if addrs := h.Addrs(); len(addrs) > 0 {
113
+ for _, a := range addrs {
114
+ log.Printf("[client] host addr: %s/p2p/%s", a.String(), h.ID().String())
115
+ }
116
+ } else {
117
+ log.Printf("[client] host peer: %s (no listen addrs yet)", h.ID().String())
118
+ }
119
+
120
+ // 종료 대기
121
+ sig := make(chan os.Signal, 1)
122
+ signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
123
+ <-sig
124
+ log.Println("[client] shutting down")
125
+ time.Sleep(200 * time.Millisecond)
126
+ return nil
127
+}
128
+
129
+func addrToTarget(listen string) string {
130
+ // ":8081" 같은 형식을 TargetTCP에 맞게 127.0.0.1로 보정
131
+ if len(listen) > 0 && listen[0] == ':' {
132
+ return "127.0.0.1" + listen
133
+ }
134
+ return listen
135
+}
136
+
137
+var pageTmpl = template.Must(template.New("index").Parse(`<!DOCTYPE html>
138
+<html lang="en">
139
+<head>
140
+ <meta charset="UTF-8">
141
+ <title>RelayDNS Backend</title>
142
+ <style>
143
+ body { font-family: sans-serif; background: #f9f9f9; padding: 40px; }
144
+ h1 { color: #333; }
145
+ footer { margin-top: 40px; color: #666; font-size: 0.9em; }
146
+ .card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); }
147
+ </style>
148
+</head>
149
+<body>
150
+ <div class="card">
151
+ <h1>🚀 RelayDNS Backend</h1>
152
+ <p>This page is served from the backend node.</p>
153
+ <p>Current time: <b>{{.Now}}</b></p>
154
+ <p>Hostname: <b>{{.Host}}</b></p>
155
+ </div>
156
+ <footer>relaydns demo client — served locally at {{.Addr}}</footer>
157
+</body>
158
+</html>`))
cmd/server.go
deleted
-80
@@ -1,80 +0,0 @@
1
-package main
2
-
3
-import (
4
- "context"
5
- "log"
6
- "os"
7
- "os/signal"
8
- "syscall"
9
- "time"
10
-
11
- "github.com/gosuda/relaydns/relaydns"
12
- "github.com/spf13/cobra"
13
-)
14
-
15
-var rootCmd = &cobra.Command{
16
- Use: "relayserver",
17
- Short: "A lightweight, DNS-driven peer-to-peer proxy layer built on libp2p",
18
- RunE: runServer,
19
-}
20
-
21
-var (
22
- flagBootstraps []string
23
- flagRelay bool
24
- listenTCP string
25
- listenHTTP string
26
- protocol string
27
- topic string
28
-)
29
-
30
-func init() {
31
- flags := rootCmd.PersistentFlags()
32
- flags.StringSliceVar(&flagBootstraps, "bootstrap", nil, "multiaddrs with /p2p/ (supports /dnsaddr/ that resolves to /p2p/)")
33
- flags.BoolVar(&flagRelay, "relay", true, "enable libp2p relay support")
34
- flags.StringVar(&listenTCP, "listen-tcp", ":22", "TCP listen (e.g. :22 for SSH)")
35
- flags.StringVar(&listenHTTP, "listen-http", ":8080", "HTTP admin API")
36
- flags.StringVar(&protocol, "protocol", "/relaydns/ssh/1.0", "libp2p protocol id for streams")
37
- flags.StringVar(&topic, "topic", "relaydns.backends", "pubsub topic for adverts")
38
-}
39
-
40
-func main() {
41
- if err := rootCmd.Execute(); err != nil {
42
- log.Fatal(err)
43
- }
44
-}
45
-
46
-func runServer(cmd *cobra.Command, args []string) error {
47
- ctx, cancel := context.WithCancel(context.Background())
48
- defer cancel()
49
- h, err := relaydns.MakeHost(ctx, flagRelay)
50
- if err != nil {
51
- return err
52
- }
53
- relaydns.ConnectBootstraps(ctx, h, flagBootstraps)
54
-
55
- d, err := relaydns.NewDirector(ctx, h, protocol, topic)
56
- if err != nil {
57
- return err
58
- }
59
-
60
- go func() {
61
- if err := d.ServeHTTP(listenHTTP); err != nil {
62
- log.Println("http api:", err)
63
- cancel()
64
- }
65
- }()
66
- go func() {
67
- if err := d.ServeTCP(listenTCP); err != nil {
68
- log.Println("tcp:", err)
69
- cancel()
70
- }
71
- }()
72
-
73
- // graceful shutdown
74
- sig := make(chan os.Signal, 1)
75
- signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
76
- <-sig
77
- cancel()
78
- time.Sleep(300 * time.Millisecond)
79
- return nil
80
-}
cmd/server/main.go
new
+111
@@ -0,0 +1,111 @@
1
+package main
2
+
3
+import (
4
+ "context"
5
+ "log"
6
+ "os"
7
+ "os/signal"
8
+ "syscall"
9
+ "time"
10
+
11
+ "github.com/gosuda/relaydns/relaydns"
12
+ "github.com/spf13/cobra"
13
+)
14
+
15
+var rootCmd = &cobra.Command{
16
+ Use: "relayserver",
17
+ Short: "A lightweight, DNS-driven peer-to-peer proxy layer built on libp2p",
18
+ RunE: runServer,
19
+}
20
+
21
+var (
22
+ flagBootstraps []string
23
+ flagRelay bool
24
+
25
+ ingressTCP string // e.g. :22 (raw TCP ingress, SSH)
26
+ ingressHTTP string // e.g. :8082 (HTTP ingress, Browser)
27
+ adminHTTP string // e.g. :8080 (admin API)
28
+ outBoundPort int // e.g. :4001 (outbound connections)
29
+ protocol string // e.g. /relaydns/http/1.0
30
+ topic string // e.g. relaydns.backends
31
+)
32
+
33
+func init() {
34
+ flags := rootCmd.PersistentFlags()
35
+ flags.StringSliceVar(&flagBootstraps, "bootstrap", nil, "multiaddrs with /p2p/ (supports /dnsaddr/ that resolves to /p2p/)")
36
+ flags.BoolVar(&flagRelay, "relay", true, "enable libp2p relay support")
37
+
38
+ flags.StringVar(&ingressTCP, "ingress-tcp", ":22", "L4 TCP ingress (e.g. :22 for SSH/raw TCP)")
39
+ flags.StringVar(&ingressHTTP, "ingress-http", ":8082", "HTTP ingress (browser-friendly TCP port for HTTP backends)")
40
+ flags.StringVar(&adminHTTP, "admin-http", ":8080", "Admin HTTP API (status/control)")
41
+ flags.IntVar(&outBoundPort, "outbound-port", 4001, "Outbound connections")
42
+
43
+ flags.StringVar(&protocol, "protocol", "/relaydns/http/1.0", "libp2p protocol id for streams (must match clients)")
44
+ flags.StringVar(&topic, "topic", "relaydns.backends", "pubsub topic for backend adverts")
45
+}
46
+
47
+func main() {
48
+ if err := rootCmd.Execute(); err != nil {
49
+ log.Fatal(err)
50
+ }
51
+}
52
+
53
+func runServer(cmd *cobra.Command, args []string) error {
54
+ ctx, cancel := context.WithCancel(context.Background())
55
+ defer cancel()
56
+
57
+ h, err := relaydns.MakeHost(ctx, outBoundPort, flagRelay)
58
+ if err != nil {
59
+ return err
60
+ }
61
+ relaydns.ConnectBootstraps(ctx, h, flagBootstraps)
62
+
63
+ d, err := relaydns.NewDirector(ctx, h, protocol, topic)
64
+ if err != nil {
65
+ return err
66
+ }
67
+
68
+ // 1) admin API
69
+ go func() {
70
+ if adminHTTP == "" {
71
+ return
72
+ }
73
+ log.Println("[server] admin http:", adminHTTP)
74
+ if err := d.ServeHTTP(adminHTTP); err != nil {
75
+ log.Println("[server] admin http error:", err)
76
+ cancel()
77
+ }
78
+ }()
79
+
80
+ // 2) L4 TCP ingress (SSH/raw TCP)
81
+ go func() {
82
+ if ingressTCP == "" {
83
+ return
84
+ }
85
+ log.Println("[server] tcp ingress:", ingressTCP)
86
+ if err := d.ServeTCP(ingressTCP); err != nil {
87
+ log.Println("[server] tcp ingress error:", err)
88
+ cancel()
89
+ }
90
+ }()
91
+
92
+ // 3) HTTP ingress (browser/HTTP traffic)
93
+ go func() {
94
+ if ingressHTTP == "" {
95
+ return
96
+ }
97
+ log.Println("[server] http ingress (tcp-level):", ingressHTTP)
98
+ if err := d.ServeTCP(ingressHTTP); err != nil {
99
+ log.Println("[server] http ingress error:", err)
100
+ cancel()
101
+ }
102
+ }()
103
+
104
+ // graceful shutdown
105
+ sig := make(chan os.Signal, 1)
106
+ signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
107
+ <-sig
108
+ cancel()
109
+ time.Sleep(300 * time.Millisecond)
110
+ return nil
111
+}
relaydns/client.go
+101
@@ -3,10 +3,15 @@ package relaydns
3
import (
4
"context"
5
"encoding/json"
6
+ "errors"
7
"fmt"
8
"io"
9
"log"
10
"net"
11
+ "net/http"
12
+ "net/url"
13
+ "sort"
14
+ "strings"
15
"sync"
16
"time"
17
@@ -17,6 +22,12 @@ import (
22
)
23
24
type ClientConfig struct {
25
+ ServerURL string
26
+ Bootstraps []string
27
+ HTTPTimeout time.Duration
28
+ PreferQUIC bool
29
+ PreferLocal bool
30
+
31
// libp2p stream protocol id (e.g. "/relaydns/ssh/1.0")
32
Protocol string
33
// pubsub topic for backend adverts (e.g. "relaydns.backends")
@@ -58,12 +69,34 @@ func NewClient(ctx context.Context, h host.Host, cfg ClientConfig) (*RelayClient
69
if cfg.AdvertiseEvery <= 0 {
70
cfg.AdvertiseEvery = 5 * time.Second
71
}
72
+ if cfg.HTTPTimeout <= 0 {
73
+ cfg.HTTPTimeout = 3 * time.Second
74
+ }
75
b := &RelayClient{
76
h: h,
77
cfg: cfg,
78
protoID: protocol.ID(cfg.Protocol),
79
}
80
81
+ boot := make([]string, 0, len(cfg.Bootstraps)+4)
82
+ if len(cfg.Bootstraps) > 0 {
83
+ boot = append(boot, cfg.Bootstraps...)
84
+ }
85
+ if cfg.ServerURL != "" {
86
+ if addrs, err := fetchMultiaddrsFromHealth(cfg.ServerURL, cfg.HTTPTimeout); err != nil {
87
+ log.Printf("relaydns: fetch /health from %s failed: %v", cfg.ServerURL, err)
88
+ } else {
89
+ sortMultiaddrs(addrs, cfg.PreferQUIC, cfg.PreferLocal)
90
+ boot = append(boot, addrs...)
91
+ }
92
+ }
93
+ boot = uniq(boot)
94
+ if len(boot) > 0 {
95
+ ConnectBootstraps(ctx, h, boot)
96
+ } else {
97
+ log.Printf("relaydns: no bootstrap sources provided (Bootstraps/ServerURL); discovery may fail")
98
+ }
99
+
100
// 1) stream handler
101
switch {
102
case cfg.Handler != nil:
@@ -140,3 +173,71 @@ func (b *RelayClient) Close() error {
173
// leaving topic is optional; libp2p will clean up on host close
174
return nil
175
}
176
+func fetchMultiaddrsFromHealth(base string, timeout time.Duration) ([]string, error) {
177
+ u, err := url.Parse(base)
178
+ if err != nil {
179
+ return nil, fmt.Errorf("parse server-url: %w", err)
180
+ }
181
+ // ensure path ends with /health
182
+ if !strings.HasSuffix(u.Path, "/health") {
183
+ if u.Path == "" || u.Path == "/" {
184
+ u.Path = "/health"
185
+ } else {
186
+ u.Path = strings.TrimSuffix(u.Path, "/") + "/health"
187
+ }
188
+ }
189
+ client := &http.Client{Timeout: timeout}
190
+ req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
191
+ resp, err := client.Do(req)
192
+ if err != nil {
193
+ return nil, err
194
+ }
195
+ defer resp.Body.Close()
196
+
197
+ var payload struct {
198
+ Status string `json:"status"`
199
+ PeerID string `json:"peerId"`
200
+ Multiaddrs []string `json:"multiaddrs"`
201
+ }
202
+ if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
203
+ return nil, err
204
+ }
205
+ if payload.Status != "ok" {
206
+ return nil, errors.New("health not ok")
207
+ }
208
+ addrs := make([]string, 0, len(payload.Multiaddrs))
209
+ for _, s := range payload.Multiaddrs {
210
+ // 아주 기본적인 sanity check
211
+ if strings.Contains(s, "/p2p/") && (strings.Contains(s, "/ip4/") || strings.Contains(s, "/ip6/")) {
212
+ addrs = append(addrs, s)
213
+ }
214
+ }
215
+ return addrs, nil
216
+}
217
+
218
+func sortMultiaddrs(addrs []string, preferQUIC, preferLocal bool) {
219
+ score := func(a string) int {
220
+ sc := 0
221
+ if preferQUIC && strings.Contains(a, "/quic-v1") {
222
+ sc += 2
223
+ }
224
+ if preferLocal && (strings.Contains(a, "/ip4/127.0.0.1/") || strings.Contains(a, "/ip6/::1/")) {
225
+ sc += 1
226
+ }
227
+ return sc
228
+ }
229
+ sort.SliceStable(addrs, func(i, j int) bool { return score(addrs[i]) > score(addrs[j]) })
230
+}
231
+
232
+func uniq(ss []string) []string {
233
+ seen := map[string]struct{}{}
234
+ out := make([]string, 0, len(ss))
235
+ for _, s := range ss {
236
+ if _, ok := seen[s]; ok {
237
+ continue
238
+ }
239
+ seen[s] = struct{}{}
240
+ out = append(out, s)
241
+ }
242
+ return out
243
+}
relaydns/director.go
+15
-1
@@ -3,6 +3,7 @@ package relaydns
3
import (
4
"context"
5
"encoding/json"
6
+ "fmt"
7
"io"
8
"log"
9
"net"
@@ -193,7 +194,20 @@ func (d *Director) ServeHTTP(addr string) error {
194
}
195
})
196
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
196
- _, _ = w.Write([]byte("ok"))
197
+ type info struct {
198
+ Status string `json:"status"`
199
+ Addrs []string `json:"multiaddrs"`
200
+ }
201
+ var list []string = make([]string, 0)
202
+ for _, a := range d.h.Addrs() {
203
+ list = append(list, fmt.Sprintf("%s/p2p/%s", a.String(), d.h.ID().String()))
204
+ }
205
+ resp := info{
206
+ Status: "ok",
207
+ Addrs: list,
208
+ }
209
+ w.Header().Set("Content-Type", "application/json")
210
+ _ = json.NewEncoder(w).Encode(resp)
211
})
212
log.Printf("director HTTP API on %s", addr)
213
return http.ListenAndServe(addr, mux)
relaydns/host.go
+12
-3
@@ -2,6 +2,7 @@ package relaydns
2
3
import (
4
"context"
5
+ "fmt"
6
"log"
7
8
"github.com/libp2p/go-libp2p"
@@ -10,14 +11,22 @@ import (
11
ma "github.com/multiformats/go-multiaddr"
12
)
13
13
-func MakeHost(ctx context.Context, enableRelay bool) (host.Host, error) {
14
+func MakeHost(ctx context.Context, port int, enableRelay bool) (host.Host, error) {
15
+ addrs := []string{
16
+ fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port),
17
+ fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1", port),
18
+ fmt.Sprintf("/ip6/::/tcp/%d", port),
19
+ fmt.Sprintf("/ip6/::/udp/%d/quic-v1", port),
20
+ }
21
+
22
opts := []libp2p.Option{
15
- libp2p.DefaultTransports, // TCP+QUIC
23
+ libp2p.ListenAddrStrings(addrs...),
24
+ libp2p.DefaultTransports, // TCP+QUIC
25
+ libp2p.NATPortMap(),
26
libp2p.EnableNATService(), // AutoNAT helper
27
libp2p.EnableHolePunching(), // DCUtR
28
libp2p.DefaultSecurity,
29
libp2p.DefaultMuxers,
20
- libp2p.EnableAutoRelay(), // ← 추가
30
}
31
if enableRelay {
32
opts = append(opts, libp2p.EnableRelay()) // circuit relay (useful both as client & svc)