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
)