main
go 287 lines 8.44 KB
Raw
1 package main
2
3 import (
4 "bytes"
5 "context"
6 "errors"
7 "flag"
8 "fmt"
9 "io"
10 "net"
11 "os"
12
13 "github.com/rs/zerolog"
14 "github.com/rs/zerolog/log"
15
16 "github.com/gosuda/portal-tunnel/v2/sdk"
17 "github.com/gosuda/portal-tunnel/v2/types"
18 "github.com/gosuda/portal-tunnel/v2/utils"
19 )
20
21 func main() {
22 log.Logger = log.Output(zerolog.NewConsoleWriter())
23 if err := utils.RunCommands(os.Args[1:], os.Stdout, os.Stderr, printRootUsage, map[string]utils.CommandFunc{
24 "": runTCPCommand,
25 "tcp": runTCPCommand,
26 "udp": runUDPCommand,
27 "help": utils.MakeHelpCommand(printRootUsage, []utils.HelpTopic{
28 {Name: "tcp", Usage: printTCPUsage},
29 {Name: "udp", Usage: printUDPUsage},
30 }),
31 }); err != nil {
32 log.Error().Err(err).Msg("demo command failed")
33 os.Exit(1)
34 }
35 }
36
37 type demoConfig struct {
38 relayURLs string
39 discovery bool
40 banMITM bool
41 identityPath string
42 identityJSON string
43 addr string
44 name string
45 desc string
46 tags string
47 owner string
48 hide bool
49 thumbnail string
50 maxActiveRelays int
51 }
52
53 // registerConnectivityFlags registers the relay, discovery, identity, and
54 // owner flags that are shared across TCP and UDP demo commands.
55 func registerConnectivityFlags(fs *flag.FlagSet, cfg *demoConfig, defaultRelays string) {
56 utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", defaultRelays, "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with bootstrap relays when discovery is enabled)", "RELAYS")
57 utils.BoolFlagEnv(fs, &cfg.discovery, "discovery", true, "include bootstrap relays and enable discovery", "DISCOVERY")
58 utils.BoolFlagEnv(fs, &cfg.banMITM, "ban-mitm", false, "ban relay when the MITM self-probe detects TLS termination", "BAN_MITM")
59 utils.StringFlagEnv(fs, &cfg.identityPath, "identity-path", "identity.json", "identity json file path", "IDENTITY_PATH")
60 utils.StringFlagEnv(fs, &cfg.identityJSON, "identity-json", "", "identity json payload; overrides --identity-path contents and is persisted there when both are set", "IDENTITY_JSON")
61 utils.IntFlagEnv(fs, &cfg.maxActiveRelays, "max-active-relays", 3, nil, "maximum number of auto-selected relays to keep connected; explicit --relays are always included", "MAX_ACTIVE_RELAYS")
62 utils.StringFlag(fs, &cfg.owner, "owner", "PortalApp Developer", "lease owner")
63 }
64
65 func runTCPCommand(args []string) error {
66 cfg := demoConfig{}
67
68 fs := utils.NewFlagSet("demo-app", printTCPUsage)
69 registerConnectivityFlags(fs, &cfg, "https://gosunuts.xyz")
70 utils.StringFlag(fs, &cfg.addr, "addr", "127.0.0.1:8092", "local demo HTTP listen address (host:port or URL; disable if empty)")
71 utils.StringFlag(fs, &cfg.name, "name", "demo-app", "public hostname prefix (single DNS label)")
72 utils.StringFlag(fs, &cfg.desc, "description", "Portal demo connectivity app", "lease description")
73 utils.StringFlag(fs, &cfg.tags, "tags", "demo,connectivity,activity,cloud,sun,morning", "comma-separated lease tags")
74 utils.StringFlag(fs, &cfg.thumbnail, "thumbnail", "https://picsum.photos/640/360", "lease thumbnail")
75 utils.BoolFlag(fs, &cfg.hide, "hide", false, "hide this lease from listings")
76
77 if err := utils.ParseFlagSet(fs, args, printTCPUsage); err != nil {
78 if errors.Is(err, flag.ErrHelp) {
79 return nil
80 }
81 return err
82 }
83 if err := utils.RequireNoArgs(fs.Args(), "demo-app"); err != nil {
84 printTCPUsage(os.Stderr)
85 return err
86 }
87 normalizedName, err := utils.NormalizeDNSLabel(cfg.name)
88 if err != nil {
89 return fmt.Errorf("invalid --name value: %w", err)
90 }
91 cfg.name = normalizedName
92
93 ctx, stop := utils.SignalContext()
94 defer stop()
95
96 return runTCPDemo(ctx, cfg)
97 }
98
99 func runUDPCommand(args []string) error {
100 cfg := demoConfig{}
101 fs := utils.NewFlagSet("demo-app-udp", printUDPUsage)
102
103 registerConnectivityFlags(fs, &cfg, "https://localhost:4017")
104 utils.StringFlag(fs, &cfg.name, "name", "demo-udp", "public hostname prefix (single DNS label)")
105 utils.StringFlag(fs, &cfg.desc, "description", "Portal demo UDP echo service", "lease description")
106 utils.StringFlag(fs, &cfg.tags, "tags", "demo,udp,echo", "comma-separated lease tags")
107 utils.StringFlag(fs, &cfg.thumbnail, "thumbnail", "", "lease thumbnail")
108 utils.BoolFlag(fs, &cfg.hide, "hide", true, "hide this lease from listings")
109
110 if err := utils.ParseFlagSet(fs, args, printUDPUsage); err != nil {
111 if errors.Is(err, flag.ErrHelp) {
112 return nil
113 }
114 return err
115 }
116 if err := utils.RequireNoArgs(fs.Args(), "udp"); err != nil {
117 printUDPUsage(os.Stderr)
118 return err
119 }
120 normalizedName, err := utils.NormalizeDNSLabel(cfg.name)
121 if err != nil {
122 return fmt.Errorf("invalid --name value: %w", err)
123 }
124 cfg.name = normalizedName
125
126 ctx, stop := utils.SignalContext()
127 defer stop()
128
129 return runUDPDemo(ctx, cfg)
130 }
131
132 func runTCPDemo(ctx context.Context, cfg demoConfig) error {
133 metadata := types.LeaseMetadata{
134 Description: cfg.desc,
135 Tags: utils.SplitCSV(cfg.tags),
136 Owner: cfg.owner,
137 Thumbnail: cfg.thumbnail,
138 Hide: cfg.hide,
139 }
140 exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
141 RelayURLs: utils.SplitCSV(cfg.relayURLs),
142 Discovery: cfg.discovery,
143 Identity: types.Identity{Name: cfg.name},
144 IdentityPath: cfg.identityPath,
145 IdentityJSON: cfg.identityJSON,
146 BanMITM: cfg.banMITM,
147 MaxActiveRelays: cfg.maxActiveRelays,
148 Metadata: metadata,
149 })
150 if err != nil {
151 return fmt.Errorf("exposure listen error: %w", err)
152 }
153
154 rawAddr := cfg.addr
155 cfg.addr, err = utils.NormalizeTargetAddr(cfg.addr)
156 if err != nil {
157 return fmt.Errorf("invalid --addr value %q: %w", rawAddr, err)
158 }
159 defer exposure.Close()
160 err = exposure.RunHTTP(ctx, newHandler(), cfg.addr)
161 if err != nil {
162 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
163 err = nil
164 }
165 return err
166 }
167
168 if ctx.Err() != nil {
169 log.Info().Msg("demo app shutting down")
170 }
171 log.Info().Msg("demo app shutdown complete")
172 return nil
173 }
174
175 func runUDPDemo(ctx context.Context, cfg demoConfig) error {
176 exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
177 RelayURLs: utils.SplitCSV(cfg.relayURLs),
178 Discovery: cfg.discovery,
179 Identity: types.Identity{Name: cfg.name},
180 IdentityPath: cfg.identityPath,
181 IdentityJSON: cfg.identityJSON,
182 UDPEnabled: true,
183 BanMITM: cfg.banMITM,
184 MaxActiveRelays: cfg.maxActiveRelays,
185 Metadata: types.LeaseMetadata{
186 Description: cfg.desc,
187 Tags: utils.SplitCSV(cfg.tags),
188 Owner: cfg.owner,
189 Thumbnail: cfg.thumbnail,
190 Hide: cfg.hide,
191 },
192 })
193 if err != nil {
194 return fmt.Errorf("exposure listen error: %w", err)
195 }
196 defer exposure.Close()
197
198 udpAddrs, err := exposure.WaitDatagramReady(ctx)
199 if err != nil {
200 return fmt.Errorf("wait for udp readiness: %w", err)
201 }
202 for _, udpAddr := range udpAddrs {
203 log.Info().Str("udp_addr", udpAddr).Msg("demo udp relay ready")
204 }
205
206 go runUDPEchoLoop(ctx, exposure)
207
208 if err := exposure.RunHTTP(ctx, newUDPInfoHandler(exposure), ""); err != nil {
209 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
210 err = nil
211 }
212 return err
213 }
214
215 if ctx.Err() != nil {
216 log.Info().Msg("demo udp shutting down")
217 }
218 log.Info().Msg("demo udp shutdown complete")
219 return nil
220 }
221
222 func runUDPEchoLoop(ctx context.Context, exposure *sdk.Exposure) {
223 for {
224 frame, err := exposure.AcceptDatagram()
225 if err != nil {
226 if ctx.Err() != nil || errors.Is(err, net.ErrClosed) {
227 return
228 }
229 log.Warn().Err(err).Msg("demo udp accept failed")
230 return
231 }
232
233 payload := bytes.Clone(frame.Payload)
234 if len(payload) == 0 {
235 payload = []byte("pong")
236 }
237 frame.Payload = payload
238 if err := exposure.SendDatagram(frame); err != nil && ctx.Err() == nil && !errors.Is(err, net.ErrClosed) {
239 log.Warn().Err(err).Uint32("flow_id", frame.FlowID).Msg("demo udp reply failed")
240 return
241 }
242 }
243 }
244
245 func printRootUsage(w io.Writer) {
246 utils.WriteCommandUsage(w,
247 []string{
248 "demo-app [flags]",
249 "demo-app tcp [flags]",
250 "demo-app udp [flags]",
251 "demo-app help",
252 },
253 []string{
254 "demo-app",
255 "demo-app --name my-app",
256 "demo-app tcp --addr 127.0.0.1:9000",
257 "demo-app udp",
258 },
259 )
260 }
261
262 func printTCPUsage(w io.Writer) {
263 utils.WriteCommandUsage(w,
264 []string{
265 "demo-app [flags]",
266 "demo-app tcp [flags]",
267 },
268 []string{
269 "demo-app",
270 "demo-app --name my-app",
271 "demo-app tcp --addr 127.0.0.1:9000",
272 },
273 )
274 }
275
276 func printUDPUsage(w io.Writer) {
277 utils.WriteCommandUsage(w,
278 []string{
279 "demo-app udp [flags]",
280 },
281 []string{
282 "demo-app udp",
283 "demo-app udp --name my-udp-demo",
284 "demo-app udp --discovery=true",
285 },
286 )
287 }