main
go 262 lines 11.8 KB
Raw
1 package main
2
3 import (
4 "context"
5 "errors"
6 "flag"
7 "fmt"
8 "io"
9 "os"
10 "strings"
11
12 "github.com/rs/zerolog"
13 "github.com/rs/zerolog/log"
14
15 "github.com/gosuda/portal-tunnel/v2/portal"
16 "github.com/gosuda/portal-tunnel/v2/portal/acme"
17 "github.com/gosuda/portal-tunnel/v2/portal/identity"
18 "github.com/gosuda/portal-tunnel/v2/portal/overlay"
19 portalx402 "github.com/gosuda/portal-tunnel/v2/portal/x402"
20 "github.com/gosuda/portal-tunnel/v2/types"
21 "github.com/gosuda/portal-tunnel/v2/utils"
22 )
23
24 func main() {
25 log.Logger = log.Output(zerolog.NewConsoleWriter())
26 if err := utils.RunCommands(os.Args[1:], os.Stdout, os.Stderr, printRootUsage, map[string]utils.CommandFunc{
27 "": runServeCommand,
28 "serve": runServeCommand,
29 "help": runHelpCommand,
30 }); err != nil {
31 log.Error().Err(err).Msg("execute root command")
32 os.Exit(1)
33 }
34 }
35
36 type relayServerConfig struct {
37 PortalURL string
38 IdentityPath string
39 Bootstraps string
40 DiscoveryEnabled bool
41 WireGuardPort int
42 APIPort int
43 SNIPort int
44 TrustProxyHeaders bool
45 TrustedProxyCIDRs string
46 UDPEnabled bool
47 TCPEnabled bool
48 MinPort int
49 MaxPort int
50 AdminToken string
51 PProfEnabled bool
52 PProfAddr string
53 X402Enabled bool
54 X402Testnet bool
55 X402PayTo string
56
57 ACMEDNSProvider string
58 ENSGaslessEnabled bool
59 CloudflareToken string
60 GCPProjectID string
61 GCPManagedZone string
62 HetznerAPIToken string
63 AWSAccessKeyID string
64 AWSSecretAccessKey string
65 AWSSessionToken string
66 AWSRegion string
67 AWSHostedZoneID string
68 AWSDNSSECKMSKeyARN string
69 VultrAPIKey string
70 NjallaToken string
71 }
72
73 func runServeCommand(args []string) error {
74 cfg := relayServerConfig{}
75 fs := utils.NewFlagSet("relay-server", printRootUsage)
76
77 utils.StringFlagEnv(fs, &cfg.PortalURL, "portal-url", "https://localhost:4017", "portal base URL", "PORTAL_URL")
78 utils.StringFlagEnv(fs, &cfg.IdentityPath, "identity-path", "./.portal-certs", "directory path for relay identity, policy state, and keyless materials", "IDENTITY_PATH")
79 utils.StringFlagEnv(fs, &cfg.Bootstraps, "bootstraps", "", "bootstrap relay API URLs; merged with bootstrap relays when discovery is enabled", "BOOTSTRAPS")
80 utils.BoolFlagEnv(fs, &cfg.DiscoveryEnabled, "discovery", false, "serve relay discovery endpoints and poll discovery peers", "DISCOVERY")
81 utils.IntFlagEnv(fs, &cfg.WireGuardPort, "wireguard-port", overlay.DefaultListenPort, utils.ParsePortNumber, "public and listen UDP port for relay overlay", "WIREGUARD_PORT")
82
83 utils.IntFlagEnv(fs, &cfg.APIPort, "api-port", 4017, utils.ParsePortNumber, "Admin/API server port", "API_PORT")
84 utils.IntFlagEnv(fs, &cfg.SNIPort, "sni-port", 443, utils.ParsePortNumber, "TCP SNI router port number", "SNI_PORT")
85 utils.BoolFlagEnv(fs, &cfg.TrustProxyHeaders, "trust-proxy-headers", false, "trust X-Forwarded-* and X-Real-IP headers from trusted proxies", "TRUST_PROXY_HEADERS")
86 utils.StringFlagEnv(fs, &cfg.TrustedProxyCIDRs, "trusted-proxy-cidrs", "", "trusted proxy CIDR allowlist for forwarded headers, comma-separated; defaults to private/loopback proxy ranges when trust-proxy-headers is enabled", "TRUSTED_PROXY_CIDRS")
87
88 utils.BoolFlagEnv(fs, &cfg.UDPEnabled, "udp-enabled", false, "enable UDP relay transport; requires a valid --min-port/--max-port range", "UDP_ENABLED")
89 utils.BoolFlagEnv(fs, &cfg.TCPEnabled, "tcp-enabled", false, "enable raw TCP port transport; requires a valid --min-port/--max-port range", "TCP_ENABLED")
90 utils.IntFlagEnv(fs, &cfg.MinPort, "min-port", 0, utils.ParseOptionalPortNumber, "inclusive minimum lease port shared by UDP and raw TCP transports (0=disabled)", "MIN_PORT")
91 utils.IntFlagEnv(fs, &cfg.MaxPort, "max-port", 0, utils.ParseOptionalPortNumber, "inclusive maximum lease port shared by UDP and raw TCP transports (0=disabled)", "MAX_PORT")
92
93 utils.StringFlagEnv(fs, &cfg.AdminToken, "admin-token", "", "admin bearer token for relay admin and policy APIs", "ADMIN_TOKEN")
94 utils.BoolFlagEnv(fs, &cfg.PProfEnabled, "pprof-enabled", false, "enable pprof diagnostics HTTP server", "PPROF_ENABLED")
95 utils.StringFlagEnv(fs, &cfg.PProfAddr, "pprof-addr", portal.DefaultPProfListenAddr, "pprof diagnostics listen address when enabled", "PPROF_ADDR")
96 utils.BoolFlagEnv(fs, &cfg.X402Enabled, "x402-enabled", false, "enable relay-owned Sui x402 facilitator endpoints under /api/x402 for future control-plane payments", "X402_ENABLED")
97 utils.BoolFlagEnv(fs, &cfg.X402Testnet, "x402-testnet", false, "use Sui testnet for relay-owned x402 facilitator payments", "X402_TESTNET")
98 utils.StringFlagEnv(fs, &cfg.X402PayTo, "x402-pay-to", "", "Sui payment recipient address for relay-owned control-plane x402 resources", "X402_PAY_TO")
99
100 utils.StringFlagEnv(fs, &cfg.ACMEDNSProvider, "acme-dns-provider", "", "DNS provider for managed DNS-01/A-record sync, ECH HTTPS records, and ENS gasless DNSSEC/TXT automation (cloudflare|gcloud|hetzner|njalla|route53|vultr); leave empty to use manual fullchain.pem/privatekey.pem from IDENTITY_PATH", "ACME_DNS_PROVIDER")
101 utils.BoolFlagEnv(fs, &cfg.ENSGaslessEnabled, "ens-gasless-enabled", false, "enable ENS gasless DNS import automation for the managed DNS zone and lease hostnames", "ENS_GASLESS_ENABLED")
102 utils.StringFlagEnv(fs, &cfg.CloudflareToken, "cloudflare-token", "", "Cloudflare DNS API token (required when acme-dns-provider=cloudflare)", "CLOUDFLARE_TOKEN")
103 utils.StringFlagEnv(fs, &cfg.GCPProjectID, "gcp-project-id", "", "Google Cloud project id for Cloud DNS automation; auto-detected from ADC or GCE metadata when omitted", "GCP_PROJECT_ID", "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT", "GCE_PROJECT")
104 utils.StringFlagEnv(fs, &cfg.GCPManagedZone, "gcp-managed-zone", "", "explicit Google Cloud DNS managed zone name or numeric ID override", "GCP_MANAGED_ZONE", "GCP_ZONE", "GCE_ZONE_ID")
105 utils.StringFlagEnv(fs, &cfg.HetznerAPIToken, "hetzner-api-token", "", "Hetzner Cloud API token for DNS automation (required when acme-dns-provider=hetzner)", "HETZNER_API_TOKEN", "HCLOUD_TOKEN")
106 utils.StringFlagEnv(fs, &cfg.AWSAccessKeyID, "aws-access-key-id", "", "AWS access key ID for Route53 static credentials; uses the default AWS credential chain when omitted", "AWS_ACCESS_KEY_ID")
107 utils.StringFlagEnv(fs, &cfg.AWSSecretAccessKey, "aws-secret-access-key", "", "AWS secret access key for Route53 static credentials", "AWS_SECRET_ACCESS_KEY")
108 utils.StringFlagEnv(fs, &cfg.AWSSessionToken, "aws-session-token", "", "AWS session token for Route53 temporary credentials", "AWS_SESSION_TOKEN")
109 utils.StringFlagEnv(fs, &cfg.AWSRegion, "aws-region", "", "AWS region for Route53 and Route53-backed DNS-01; defaults to us-east-1 when unset", "AWS_REGION", "AWS_DEFAULT_REGION")
110 utils.StringFlagEnv(fs, &cfg.AWSHostedZoneID, "aws-hosted-zone-id", "", "explicit Route53 hosted zone ID override", "AWS_HOSTED_ZONE_ID")
111 utils.StringFlagEnv(fs, &cfg.AWSDNSSECKMSKeyARN, "aws-dnssec-kms-key-arn", "", "AWS KMS key ARN used to create a Route53 DNSSEC key-signing key when needed", "AWS_DNSSEC_KMS_KEY_ARN")
112 utils.StringFlagEnv(fs, &cfg.VultrAPIKey, "vultr-api-key", "", "Vultr API key for DNS automation (required when acme-dns-provider=vultr)", "VULTR_API_KEY")
113 utils.StringFlagEnv(fs, &cfg.NjallaToken, "njalla-token", "", "Njalla API token for DNS automation (required when acme-dns-provider=njalla)", "NJALLA_TOKEN")
114
115 if err := utils.ParseFlagSet(fs, args, printRootUsage); err != nil {
116 if errors.Is(err, flag.ErrHelp) {
117 return nil
118 }
119 return err
120 }
121 if err := utils.RequireNoArgs(fs.Args(), "relay-server"); err != nil {
122 printRootUsage(os.Stderr)
123 return err
124 }
125 cfg.IdentityPath = identity.ResolveRelayStateDir(cfg.IdentityPath)
126
127 log.Info().
128 Str("release_version", types.ReleaseVersion).
129 Str("portal_url", cfg.PortalURL).
130 Str("identity_path", cfg.IdentityPath).
131 Str("bootstraps", cfg.Bootstraps).
132 Bool("discovery_enabled", cfg.DiscoveryEnabled).
133 Int("wireguard_port", cfg.WireGuardPort).
134 Int("api_port", cfg.APIPort).
135 Int("sni_port", cfg.SNIPort).
136 Bool("trust_proxy_headers", cfg.TrustProxyHeaders).
137 Str("trusted_proxy_cidrs", cfg.TrustedProxyCIDRs).
138 Bool("udp_enabled", cfg.UDPEnabled).
139 Bool("tcp_enabled", cfg.TCPEnabled).
140 Int("min_port", cfg.MinPort).
141 Int("max_port", cfg.MaxPort).
142 Bool("admin_token_configured", strings.TrimSpace(cfg.AdminToken) != "").
143 Bool("pprof_enabled", cfg.PProfEnabled).
144 Str("pprof_addr", cfg.PProfAddr).
145 Bool("x402_facilitator_enabled", cfg.X402Enabled).
146 Bool("x402_testnet", cfg.X402Testnet).
147 Bool("x402_pay_to_configured", strings.TrimSpace(cfg.X402PayTo) != "").
148 Str("acme_dns_provider", cfg.ACMEDNSProvider).
149 Bool("ens_gasless_enabled", cfg.ENSGaslessEnabled).
150 Msg("configured relay server")
151
152 ctx, stop := utils.SignalContext()
153 defer stop()
154
155 return runServer(ctx, cfg)
156 }
157
158 func runServer(ctx context.Context, cfg relayServerConfig) error {
159 server, err := portal.NewServer(portal.ServerConfig{
160 PortalURL: cfg.PortalURL,
161 IdentityPath: cfg.IdentityPath,
162 Bootstraps: utils.SplitCSV(cfg.Bootstraps),
163 DiscoveryEnabled: cfg.DiscoveryEnabled,
164 WireGuardPort: cfg.WireGuardPort,
165 APIPort: cfg.APIPort,
166 SNIPort: cfg.SNIPort,
167 TrustProxyHeaders: cfg.TrustProxyHeaders,
168 TrustedProxyCIDRs: cfg.TrustedProxyCIDRs,
169 UDPEnabled: cfg.UDPEnabled,
170 TCPEnabled: cfg.TCPEnabled,
171 MinPort: cfg.MinPort,
172 MaxPort: cfg.MaxPort,
173 PProfEnabled: cfg.PProfEnabled,
174 PProfListenAddr: cfg.PProfAddr,
175 X402Enabled: cfg.X402Enabled,
176 X402Testnet: cfg.X402Testnet,
177 X402PayTo: cfg.X402PayTo,
178 ACME: acme.Config{
179 KeyDir: cfg.IdentityPath,
180 DNSProvider: cfg.ACMEDNSProvider,
181 ENSGaslessEnabled: cfg.ENSGaslessEnabled,
182 CloudflareToken: cfg.CloudflareToken,
183 GCPProjectID: cfg.GCPProjectID,
184 GCPManagedZone: cfg.GCPManagedZone,
185 HetznerAPIToken: cfg.HetznerAPIToken,
186 AWSAccessKeyID: cfg.AWSAccessKeyID,
187 AWSSecretAccessKey: cfg.AWSSecretAccessKey,
188 AWSSessionToken: cfg.AWSSessionToken,
189 AWSRegion: cfg.AWSRegion,
190 AWSHostedZoneID: cfg.AWSHostedZoneID,
191 AWSKMSKeyARN: cfg.AWSDNSSECKMSKeyARN,
192 VultrAPIKey: cfg.VultrAPIKey,
193 NjallaToken: cfg.NjallaToken,
194 },
195 })
196 if err != nil {
197 return fmt.Errorf("create relay server: %w", err)
198 }
199
200 relayAPI, err := NewRelayAPI(server, cfg.IdentityPath, cfg.AdminToken)
201 if err != nil {
202 return fmt.Errorf("create relay api: %w", err)
203 }
204
205 apiMux := relayAPI.Handler()
206 if cfg.X402Enabled {
207 x402Network := portalx402.Network(cfg.X402Testnet)
208 if err := portalx402.MountFacilitator(apiMux, portalx402.FacilitatorConfig{
209 Testnet: cfg.X402Testnet,
210 }); err != nil {
211 return fmt.Errorf("mount x402 facilitator: %w", err)
212 }
213 log.Info().
214 Str("path", types.PathX402Facilitator).
215 Str("network", x402Network).
216 Msg("relay-owned x402 facilitator enabled")
217 }
218
219 if err := server.Start(ctx, apiMux); err != nil {
220 return fmt.Errorf("start relay server: %w", err)
221 }
222
223 return server.Wait()
224 }
225
226 func runHelpCommand(args []string) error {
227 switch len(args) {
228 case 0:
229 printRootUsage(os.Stdout)
230 return nil
231 case 1:
232 switch strings.TrimSpace(args[0]) {
233 case "", "help", "-h", "--help", "serve":
234 printRootUsage(os.Stdout)
235 return nil
236 default:
237 printRootUsage(os.Stderr)
238 return fmt.Errorf("unknown help topic %q", strings.TrimSpace(args[0]))
239 }
240 default:
241 printRootUsage(os.Stderr)
242 return errors.New("only one help topic is supported")
243 }
244 }
245
246 func printRootUsage(w io.Writer) {
247 utils.WriteCommandUsage(w,
248 []string{
249 "relay-server [flags]",
250 "relay-server serve [flags]",
251 "relay-server help",
252 },
253 []string{
254 "relay-server",
255 "relay-server serve",
256 "relay-server --portal-url https://portal.example.com",
257 "relay-server --discovery --bootstraps https://bootstrap.example.com",
258 "relay-server --udp-enabled --min-port 40000 --max-port 40099",
259 "relay-server help",
260 },
261 )
262 }