refactor: remove unused config handling and tidy codes

Kim committed Dec 11, 2025 at 21:51 UTC 7f2200e9dfef61de346a3a6423460ac5d51c1baa
3 files changed +91 -268
cmd/portal-tunnel/config.go deleted
-82
@@ -1,82 +0,0 @@
1 -package main
2 -
3 -import (
4 - "fmt"
5 - "os"
6 - "strings"
7 -
8 - "gopkg.in/yaml.v3"
9 - "gosuda.org/portal/sdk"
10 -)
11 -
12 -var defaultProtocols = []string{"http/1.1", "h2"}
13 -
14 -// ServiceConfig describes a local service exposed through the tunnel.
15 -type ServiceConfig struct {
16 - Name string `yaml:"name"`
17 - Target string `yaml:"target"`
18 - Protocols []string `yaml:"protocols"`
19 - Metadata sdk.Metadata `yaml:"metadata,omitempty"`
20 -}
21 -
22 -// TunnelConfig represents the YAML configuration schema for portal-tunnel.
23 -type TunnelConfig struct {
24 - Relays []string `yaml:"relays"`
25 - Service ServiceConfig `yaml:"service"`
26 -}
27 -
28 -// LoadConfig reads the YAML file at path, parses it into TunnelConfig, and validates it for single-service use.
29 -func LoadConfig(path string) (*TunnelConfig, error) {
30 - data, err := os.ReadFile(path)
31 - if err != nil {
32 - return nil, fmt.Errorf("read config: %w", err)
33 - }
34 -
35 - var cfg TunnelConfig
36 - if err := yaml.Unmarshal(data, &cfg); err != nil {
37 - return nil, fmt.Errorf("parse config: %w", err)
38 - }
39 - if len(cfg.Service.Protocols) == 0 {
40 - cfg.Service.Protocols = append([]string(nil), defaultProtocols...)
41 - }
42 -
43 - if err := cfg.validate(); err != nil {
44 - return nil, err
45 - }
46 -
47 - return &cfg, nil
48 -}
49 -
50 -func (cfg *TunnelConfig) validate() error {
51 - var errs []string
52 -
53 - if len(cfg.Relays) == 0 {
54 - errs = append(errs, "at least one relay must be defined")
55 - }
56 - for i, url := range cfg.Relays {
57 - if strings.TrimSpace(url) == "" {
58 - errs = append(errs, fmt.Sprintf("relays[%d]: url cannot be empty", i))
59 - }
60 - }
61 -
62 - service := cfg.Service
63 - name := strings.TrimSpace(service.Name)
64 - if name == "" {
65 - errs = append(errs, "service: name is required")
66 - }
67 - target := strings.TrimSpace(service.Target)
68 - if target == "" {
69 - errs = append(errs, "service: target is required")
70 - }
71 - for i, proto := range service.Protocols {
72 - if strings.TrimSpace(proto) == "" {
73 - errs = append(errs, fmt.Sprintf("service.protocols[%d]: protocol cannot be empty", i))
74 - }
75 - }
76 -
77 - if len(errs) > 0 {
78 - return fmt.Errorf("invalid config:\n - %s", strings.Join(errs, "\n - "))
79 - }
80 -
81 - return nil
82 -}
cmd/portal-tunnel/main.go
+90 -185
@@ -26,12 +26,12 @@ var bufferPool = sync.Pool{
26 type Config struct {
27 _ struct{} `version:"0.0.1" command:"portal-tunnel" about:"Expose local services through Portal relay"`
28
29 - ConfigPath string `flag:"config" alias:"c" env:"TUNNEL_CONFIG" about:"Path to portal-tunnel config file"`
30 - RelayURLs string `flag:"relay" env:"RELAYS" default:"ws://localhost:4017/relay" about:"Portal relay server URLs when config is not provided (comma-separated)"`
31 - Host string `flag:"host" env:"APP_HOST" about:"target host to proxy to when config is not provided (host:port or URL)"`
32 - Name string `flag:"name" env:"APP_NAME" about:"Service name when config is not provided"`
29 + RelayURLs string `flag:"relay" env:"RELAYS" default:"ws://localhost:4017/relay" about:"Portal relay server URLs (comma-separated)"`
30 + Host string `flag:"host" env:"APP_HOST" about:"target host to proxy to (host:port or URL)"`
31 + Name string `flag:"name" env:"APP_NAME" about:"Service name"`
32
33 // Metadata
34 + Protocols string `flag:"protocols" env:"APP_PROTOCOLS" default:"http/1.1,h2" about:"ALPN protocols (comma-separated)"`
35 Description string `flag:"description" env:"APP_DESCRIPTION" about:"Service description metadata"`
36 Tags string `flag:"tags" env:"APP_TAGS" about:"Service tags metadata (comma-separated)"`
37 Thumbnail string `flag:"thumbnail" env:"APP_THUMBNAIL" about:"Service thumbnail URL metadata"`
@@ -43,208 +43,88 @@ func main() {
43 var cfg Config
44 app, err := broccoli.NewApp(&cfg)
45 if err != nil {
46 - fmt.Fprintf(os.Stderr, "Error creating app: %v\n", err)
46 + log.Error().Err(err).Msg("Failed to create app")
47 os.Exit(1)
48 }
49
50 - _, _, err = app.Bind(&cfg, os.Args[1:])
51 - if err != nil {
50 + if _, _, err = app.Bind(&cfg, os.Args[1:]); err != nil {
51 if err == broccoli.ErrHelp {
53 - fmt.Print(app.Help())
52 + log.Info().Msg(app.Help())
53 os.Exit(0)
54 }
56 - fmt.Fprintf(os.Stderr, "Error: %v\n\n", err)
57 - fmt.Print(app.Help())
55 + log.Error().Err(err).Str("help", app.Help()).Msg("Failed to bind CLI arguments")
56 os.Exit(1)
57 }
58
61 - var runErr error
62 - if cfg.ConfigPath != "" {
63 - runErr = runExposeWithConfig(cfg.ConfigPath)
64 - } else {
65 - if cfg.Host == "" || cfg.Name == "" {
66 - fmt.Print(app.Help())
67 - os.Exit(1)
68 - }
69 - runErr = runExposeWithFlags(cfg)
70 - }
71 -
72 - if runErr != nil {
73 - log.Error().Err(runErr).Msg("Exited with error")
59 + if cfg.Host == "" || cfg.Name == "" {
60 + log.Error().Str("help", app.Help()).Msg("Host and name must be provided")
61 os.Exit(1)
62 }
76 -}
77 -
78 -func runExposeWithConfig(configPath string) error {
79 - cfg, err := LoadConfig(configPath)
80 - if err != nil {
81 - return fmt.Errorf("load config: %w", err)
82 - }
63
84 - relayURLs := normalizeRelayURLs(cfg.Relays)
85 - if len(relayURLs) == 0 {
86 - return fmt.Errorf("config: relays must include at least one URL")
87 - }
88 -
89 - ctx, cancel := context.WithCancel(context.Background())
90 - defer cancel()
91 -
92 - // Graceful shutdown
93 - sigCh := make(chan os.Signal, 1)
94 - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
95 - go func() {
96 - <-sigCh
97 - log.Info().Msg("")
98 - log.Info().Msg("Shutting down tunnel...")
99 - cancel()
100 - }()
101 -
102 - if err := runServiceTunnel(ctx, relayURLs, &cfg.Service, fmt.Sprintf("config=%s", configPath)); err != nil {
103 - return err
104 - }
105 -
106 - log.Info().Msg("Tunnel stopped")
107 - return nil
108 -}
109 -
110 -func runExposeWithFlags(cfg Config) error {
64 relayURLs := utils.ParseURLs(cfg.RelayURLs)
65 if len(relayURLs) == 0 {
113 - return fmt.Errorf("--relay must include at least one non-empty URL when --config is not provided")
114 - }
115 -
116 - var metadata sdk.Metadata
117 - if strings.TrimSpace(cfg.Description) != "" {
118 - metadata.Description = cfg.Description
119 - }
120 - if strings.TrimSpace(cfg.Tags) != "" {
121 - tags := strings.Split(cfg.Tags, ",")
122 - for i := range tags {
123 - tags[i] = strings.TrimSpace(tags[i])
124 - }
125 - filtered := tags[:0]
126 - for _, t := range tags {
127 - if t != "" {
128 - filtered = append(filtered, t)
129 - }
130 - }
131 - metadata.Tags = filtered
132 - }
133 - if strings.TrimSpace(cfg.Thumbnail) != "" {
134 - metadata.Thumbnail = cfg.Thumbnail
135 - }
136 - if strings.TrimSpace(cfg.Owner) != "" {
137 - metadata.Owner = cfg.Owner
138 - }
139 - if cfg.Hide {
140 - metadata.Hide = cfg.Hide
141 - }
142 -
143 - service := &ServiceConfig{
144 - Name: strings.TrimSpace(cfg.Name),
145 - Target: cfg.Host,
146 - Metadata: metadata,
66 + log.Error().Msg("--relay must include at least one non-empty URL")
67 + os.Exit(1)
68 }
148 -
69 ctx, cancel := context.WithCancel(context.Background())
70 defer cancel()
71
72 sigCh := make(chan os.Signal, 1)
73 signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
74 + defer signal.Stop(sigCh)
75 +
76 go func() {
77 <-sigCh
156 - log.Info().Msg("")
78 log.Info().Msg("Shutting down tunnel...")
79 cancel()
80 }()
81
161 - if err := runServiceTunnel(ctx, relayURLs, service, "flags"); err != nil {
162 - return err
82 + if err := runServiceTunnel(ctx, relayURLs, cfg, "flags"); err != nil {
83 + log.Error().Err(err).Msg("Exited with error")
84 + os.Exit(1)
85 }
86
87 log.Info().Msg("Tunnel stopped")
166 - return nil
167 -}
168 -
169 -func proxyConnection(ctx context.Context, localAddr string, relayConn net.Conn) error {
170 - defer relayConn.Close()
171 -
172 - localConn, err := net.Dial("tcp", localAddr)
173 - if err != nil {
174 - return fmt.Errorf("failed to connect to local service %s: %w", localAddr, err)
175 - }
176 - defer localConn.Close()
177 -
178 - errCh := make(chan error, 2)
179 - stopCh := make(chan struct{})
180 - go func() {
181 - select {
182 - case <-ctx.Done():
183 - relayConn.Close()
184 - localConn.Close()
185 - case <-stopCh:
186 - }
187 - }()
188 -
189 - go func() {
190 - buf := bufferPool.Get().([]byte)
191 - defer bufferPool.Put(buf)
192 - _, err := io.CopyBuffer(localConn, relayConn, buf)
193 - errCh <- err
194 - }()
195 -
196 - go func() {
197 - buf := bufferPool.Get().([]byte)
198 - defer bufferPool.Put(buf)
199 - _, err := io.CopyBuffer(relayConn, localConn, buf)
200 - errCh <- err
201 - }()
202 -
203 - err = <-errCh
204 - close(stopCh)
205 - relayConn.Close()
206 - <-errCh
207 -
208 - return err
88 }
89
211 -func runServiceTunnel(ctx context.Context, relayURLs []string, service *ServiceConfig, origin string) error {
212 - localAddr := service.Target
213 - serviceName := strings.TrimSpace(service.Name)
90 +func runServiceTunnel(ctx context.Context, relayURLs []string, cfg Config, origin string) error {
91 if len(relayURLs) == 0 {
92 return fmt.Errorf("no relay URLs provided")
93 }
217 - bootstrapServers := relayURLs
94 + protocols := strings.Split(cfg.Protocols, ",")
95 + if len(protocols) == 0 {
96 + protocols = []string{"http/1.1", "h2"}
97 + }
98
99 cred := sdk.NewCredential()
100 leaseID := cred.ID()
221 - if serviceName == "" {
222 - serviceName = fmt.Sprintf("tunnel-%s", leaseID[:8])
223 - log.Info().Str("service", serviceName).Msg("No service name provided; generated automatically")
101 + if cfg.Name == "" {
102 + cfg.Name = fmt.Sprintf("tunnel-%s", leaseID[:8])
103 + log.Info().Str("service", cfg.Name).Msg("No service name provided; generated automatically")
104 }
225 - log.Info().Str("service", serviceName).Msgf("Local service is reachable at %s", localAddr)
226 - log.Info().Str("service", serviceName).Msgf("Starting Portal Tunnel (%s)...", origin)
227 - log.Info().Str("service", serviceName).Msgf(" Local: %s", localAddr)
228 - log.Info().Str("service", serviceName).Msgf(" Relays: %s", strings.Join(bootstrapServers, ", "))
229 - log.Info().Str("service", serviceName).Msgf(" Lease ID: %s", leaseID)
105 + log.Info().Str("service", cfg.Name).Msgf("Local service is reachable at %s", cfg.Host)
106 + log.Info().Str("service", cfg.Name).Msgf("Starting Portal Tunnel (%s)...", origin)
107 + log.Info().Str("service", cfg.Name).Msgf(" Local: %s", cfg.Host)
108 + log.Info().Str("service", cfg.Name).Msgf(" Relays: %s", strings.Join(relayURLs, ", "))
109 + log.Info().Str("service", cfg.Name).Msgf(" Lease ID: %s", leaseID)
110
111 client, err := sdk.NewClient(func(c *sdk.ClientConfig) {
232 - c.BootstrapServers = bootstrapServers
112 + c.BootstrapServers = relayURLs
113 })
114 if err != nil {
235 - return fmt.Errorf("service %s: failed to connect to relay: %w", serviceName, err)
115 + return fmt.Errorf("service %s: failed to connect to relay: %w", cfg.Name, err)
116 }
117 defer client.Close()
118
239 - listener, err := client.Listen(cred, serviceName, service.Protocols,
240 - sdk.WithDescription(service.Metadata.Description),
241 - sdk.WithTags(service.Metadata.Tags),
242 - sdk.WithOwner(service.Metadata.Owner),
243 - sdk.WithThumbnail(service.Metadata.Thumbnail),
244 - sdk.WithHide(service.Metadata.Hide),
119 + listener, err := client.Listen(cred, cfg.Name, protocols,
120 + sdk.WithDescription(cfg.Description),
121 + sdk.WithTags(strings.Split(cfg.Tags, ",")),
122 + sdk.WithOwner(cfg.Owner),
123 + sdk.WithThumbnail(cfg.Thumbnail),
124 + sdk.WithHide(cfg.Hide),
125 )
126 if err != nil {
247 - return fmt.Errorf("service %s: failed to register service: %w", serviceName, err)
127 + return fmt.Errorf("service %s: failed to register service: %w", cfg.Name, err)
128 }
129 defer listener.Close()
130
@@ -253,13 +133,13 @@ func runServiceTunnel(ctx context.Context, relayURLs []string, service *ServiceC
133 _ = listener.Close()
134 }()
135
256 - log.Info().Str("service", serviceName).Msg("")
257 - log.Info().Str("service", serviceName).Msg("Access via:")
258 - log.Info().Str("service", serviceName).Msgf("- Name: /peer/%s", serviceName)
259 - log.Info().Str("service", serviceName).Msgf("- Lease ID: /peer/%s", leaseID)
260 - log.Info().Str("service", serviceName).Msgf("- Example: http://%s/peer/%s", bootstrapServers[0], serviceName)
136 + log.Info().Str("service", cfg.Name).Msg("")
137 + log.Info().Str("service", cfg.Name).Msg("Access via:")
138 + log.Info().Str("service", cfg.Name).Msgf("- Name: /peer/%s", cfg.Name)
139 + log.Info().Str("service", cfg.Name).Msgf("- Lease ID: /peer/%s", leaseID)
140 + log.Info().Str("service", cfg.Name).Msgf("- Example: %s/peer/%s", relayURLs[0], cfg.Name)
141
262 - log.Info().Str("service", serviceName).Msg("")
142 + log.Info().Str("service", cfg.Name).Msg("")
143
144 connCount := 0
145 var connWG sync.WaitGroup
@@ -277,39 +157,64 @@ func runServiceTunnel(ctx context.Context, relayURLs []string, service *ServiceC
157 case <-ctx.Done():
158 return nil
159 default:
280 - log.Error().Str("service", serviceName).Err(err).Msg("Failed to accept connection")
160 + log.Error().Str("service", cfg.Name).Err(err).Msg("Failed to accept connection")
161 continue
162 }
163 }
164
165 connCount++
286 - log.Info().Str("service", serviceName).Msgf("→ [#%d] New connection from %s", connCount, relayConn.RemoteAddr())
166 + log.Info().Str("service", cfg.Name).Msgf("→ [#%d] New connection from %s", connCount, relayConn.RemoteAddr())
167
168 connWG.Add(1)
169 go func(relayConn net.Conn) {
170 defer connWG.Done()
291 - if err := proxyConnection(ctx, localAddr, relayConn); err != nil {
292 - log.Error().Str("service", serviceName).Err(err).Msg("Proxy error")
171 + if err := proxyConnection(ctx, cfg.Host, relayConn); err != nil {
172 + log.Error().Str("service", cfg.Name).Err(err).Msg("Proxy error")
173 }
294 - log.Info().Str("service", serviceName).Msg("Connection closed")
174 + log.Info().Str("service", cfg.Name).Msg("Connection closed")
175 }(relayConn)
176 }
177 }
178
299 -// normalizeRelayURLs trims, de-duplicates, and filters empty relay URLs.
300 -func normalizeRelayURLs(urls []string) []string {
301 - seen := map[string]struct{}{}
302 - var out []string
303 - for _, u := range urls {
304 - u = strings.TrimSpace(u)
305 - if u == "" {
306 - continue
307 - }
308 - if _, ok := seen[u]; ok {
309 - continue
310 - }
311 - seen[u] = struct{}{}
312 - out = append(out, u)
179 +func proxyConnection(ctx context.Context, localAddr string, relayConn net.Conn) error {
180 + defer relayConn.Close()
181 +
182 + dialer := new(net.Dialer)
183 + localConn, err := dialer.DialContext(ctx, "tcp", localAddr)
184 + if err != nil {
185 + return fmt.Errorf("failed to connect to local service %s: %w", localAddr, err)
186 }
314 - return out
187 + defer localConn.Close()
188 +
189 + errCh := make(chan error, 2)
190 + stopCh := make(chan struct{})
191 + go func() {
192 + select {
193 + case <-ctx.Done():
194 + relayConn.Close()
195 + localConn.Close()
196 + case <-stopCh:
197 + }
198 + }()
199 +
200 + go func() {
201 + buf := bufferPool.Get().([]byte)
202 + defer bufferPool.Put(buf)
203 + _, err := io.CopyBuffer(localConn, relayConn, buf)
204 + errCh <- err
205 + }()
206 +
207 + go func() {
208 + buf := bufferPool.Get().([]byte)
209 + defer bufferPool.Put(buf)
210 + _, err := io.CopyBuffer(relayConn, localConn, buf)
211 + errCh <- err
212 + }()
213 +
214 + err = <-errCh
215 + close(stopCh)
216 + relayConn.Close()
217 + <-errCh
218 +
219 + return err
220 }
go.mod
+1 -1
@@ -13,7 +13,6 @@ require (
13 golang.org/x/net v0.47.0
14 google.golang.org/protobuf v1.36.10
15 gopkg.eu.org/broccoli v1.2.2
16 - gopkg.in/yaml.v3 v3.0.1
16 )
17
18 require (
@@ -23,4 +22,5 @@ require (
22 github.com/pmezard/go-difflib v1.0.0 // indirect
23 golang.org/x/sys v0.38.0 // indirect
24 golang.org/x/text v0.31.0 // indirect
25 + gopkg.in/yaml.v3 v3.0.1 // indirect
26 )