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)