Merge remote-tracking branch 'origin/prototype/reverse-proxy' into prototype/reverse-proxy

# Conflicts:
#	management/internals/modules/reverseproxy/reverseproxy.go
#	management/internals/server/boot.go
#	management/internals/shared/grpc/proxy.go
#	proxy/internal/auth/middleware.go
#	shared/management/proto/proxy_service.pb.go
#	shared/management/proto/proxy_service.proto
#	shared/management/proto/proxy_service_grpc.pb.go
This commit is contained in:
Alisdair MacLeod
2026-02-04 11:56:04 +00:00
81 changed files with 8413 additions and 458 deletions

View File

@@ -71,6 +71,8 @@ type Options struct {
DisableClientRoutes bool
// BlockInbound blocks all inbound connections from peers
BlockInbound bool
// WireguardPort is the port for the WireGuard interface. Use 0 for a random port.
WireguardPort *int
}
// validateCredentials checks that exactly one credential type is provided
@@ -140,6 +142,7 @@ func New(opts Options) (*Client, error) {
DisableServerRoutes: &t,
DisableClientRoutes: &opts.DisableClientRoutes,
BlockInbound: &opts.BlockInbound,
WireguardPort: opts.WireguardPort,
}
if opts.ConfigPath != "" {
config, err = profilemanager.UpdateOrCreateConfig(input)

View File

@@ -18,6 +18,7 @@ import (
"github.com/netbirdio/netbird/client/errors"
"github.com/netbirdio/netbird/client/iface/configurer"
"github.com/netbirdio/netbird/client/iface/device"
nbnetstack "github.com/netbirdio/netbird/client/iface/netstack"
"github.com/netbirdio/netbird/client/iface/udpmux"
"github.com/netbirdio/netbird/client/iface/wgaddr"
"github.com/netbirdio/netbird/client/iface/wgproxy"
@@ -228,6 +229,10 @@ func (w *WGIface) Close() error {
result = multierror.Append(result, fmt.Errorf("failed to close wireguard interface %s: %w", w.Name(), err))
}
if nbnetstack.IsEnabled() {
return errors.FormatErrorOrNil(result)
}
if err := w.waitUntilRemoved(); err != nil {
log.Warnf("failed to remove WireGuard interface %s: %v", w.Name(), err)
if err := w.Destroy(); err != nil {

View File

@@ -20,6 +20,7 @@ import (
"github.com/netbirdio/netbird/client/iface"
"github.com/netbirdio/netbird/client/iface/device"
"github.com/netbirdio/netbird/client/iface/netstack"
"github.com/netbirdio/netbird/client/internal/dns"
"github.com/netbirdio/netbird/client/internal/listener"
"github.com/netbirdio/netbird/client/internal/peer"
@@ -244,7 +245,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
localPeerState := peer.LocalPeerState{
IP: loginResp.GetPeerConfig().GetAddress(),
PubKey: myPrivateKey.PublicKey().String(),
KernelInterface: device.WireGuardModuleIsLoaded(),
KernelInterface: device.WireGuardModuleIsLoaded() && !netstack.IsEnabled(),
FQDN: loginResp.GetPeerConfig().GetFqdn(),
}
c.statusRecorder.UpdateLocalPeerState(localPeerState)

View File

@@ -1017,7 +1017,7 @@ func (e *Engine) updateConfig(conf *mgmProto.PeerConfig) error {
state := e.statusRecorder.GetLocalPeerState()
state.IP = e.wgInterface.Address().String()
state.PubKey = e.config.WgPrivateKey.PublicKey().String()
state.KernelInterface = device.WireGuardModuleIsLoaded()
state.KernelInterface = !e.wgInterface.IsUserspaceBind()
state.FQDN = conf.GetFqdn()
e.statusRecorder.UpdateLocalPeerState(state)

View File

@@ -10,6 +10,7 @@ import (
log "github.com/sirupsen/logrus"
firewallManager "github.com/netbirdio/netbird/client/firewall/manager"
"github.com/netbirdio/netbird/client/iface/netstack"
nftypes "github.com/netbirdio/netbird/client/internal/netflow/types"
sshauth "github.com/netbirdio/netbird/client/ssh/auth"
sshconfig "github.com/netbirdio/netbird/client/ssh/config"
@@ -94,6 +95,10 @@ func (e *Engine) updateSSH(sshConf *mgmProto.SSHConfig) error {
// updateSSHClientConfig updates the SSH client configuration with peer information
func (e *Engine) updateSSHClientConfig(remotePeers []*mgmProto.RemotePeerConfig) error {
if netstack.IsEnabled() {
return nil
}
peerInfo := e.extractPeerSSHInfo(remotePeers)
if len(peerInfo) == 0 {
log.Debug("no SSH-enabled peers found, skipping SSH config update")
@@ -216,6 +221,10 @@ func (e *Engine) GetPeerSSHKey(peerAddress string) ([]byte, bool) {
// cleanupSSHConfig removes NetBird SSH client configuration on shutdown
func (e *Engine) cleanupSSHConfig() {
if netstack.IsEnabled() {
return
}
configMgr := sshconfig.New()
if err := configMgr.RemoveSSHClientConfig(); err != nil {

View File

@@ -11,6 +11,7 @@ import (
log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"github.com/netbirdio/netbird/client/iface/netstack"
"github.com/netbirdio/netbird/client/iface/wgaddr"
"github.com/netbirdio/netbird/client/internal/lazyconn"
peerid "github.com/netbirdio/netbird/client/internal/peer/id"
@@ -74,12 +75,13 @@ func (m *Manager) createListener(peerCfg lazyconn.PeerConfig) (listener, error)
return NewUDPListener(m.wgIface, peerCfg)
}
// BindListener is only used on Windows and JS platforms:
// BindListener is used on Windows, JS, and netstack platforms:
// - JS: Cannot listen to UDP sockets
// - Windows: IP_UNICAST_IF socket option forces packets out the interface the default
// gateway points to, preventing them from reaching the loopback interface.
// BindListener bypasses this by passing data directly through the bind.
if runtime.GOOS != "windows" && runtime.GOOS != "js" {
// - Netstack: Allows multiple instances on the same host without port conflicts.
// BindListener bypasses these issues by passing data directly through the bind.
if runtime.GOOS != "windows" && runtime.GOOS != "js" && !netstack.IsEnabled() {
return NewUDPListener(m.wgIface, peerCfg)
}

View File

@@ -10,4 +10,6 @@ type Manager interface {
CreateReverseProxy(ctx context.Context, accountID, userID string, reverseProxy *ReverseProxy) (*ReverseProxy, error)
UpdateReverseProxy(ctx context.Context, accountID, userID string, reverseProxy *ReverseProxy) (*ReverseProxy, error)
DeleteReverseProxy(ctx context.Context, accountID, userID, reverseProxyID string) error
SetCertificateIssuedAt(ctx context.Context, accountID, reverseProxyID string) error
SetStatus(ctx context.Context, accountID, reverseProxyID string, status ProxyStatus) error
}

View File

@@ -3,6 +3,7 @@ package manager
import (
"context"
"fmt"
"time"
"github.com/google/uuid"
"github.com/rs/xid"
@@ -229,3 +230,40 @@ func (m *managerImpl) DeleteReverseProxy(ctx context.Context, accountID, userID,
return nil
}
// SetCertificateIssuedAt sets the certificate issued timestamp to the current time.
// Call this when receiving a gRPC notification that the certificate was issued.
func (m *managerImpl) SetCertificateIssuedAt(ctx context.Context, accountID, reverseProxyID string) error {
return m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
proxy, err := transaction.GetReverseProxyByID(ctx, store.LockingStrengthUpdate, accountID, reverseProxyID)
if err != nil {
return fmt.Errorf("failed to get reverse proxy: %w", err)
}
proxy.Meta.CertificateIssuedAt = time.Now()
if err = transaction.UpdateReverseProxy(ctx, proxy); err != nil {
return fmt.Errorf("failed to update reverse proxy certificate timestamp: %w", err)
}
return nil
})
}
// SetStatus updates the status of the reverse proxy (e.g., "active", "tunnel_not_created", etc.)
func (m *managerImpl) SetStatus(ctx context.Context, accountID, reverseProxyID string, status reverseproxy.ProxyStatus) error {
return m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
proxy, err := transaction.GetReverseProxyByID(ctx, store.LockingStrengthUpdate, accountID, reverseProxyID)
if err != nil {
return fmt.Errorf("failed to get reverse proxy: %w", err)
}
proxy.Meta.Status = string(status)
if err = transaction.UpdateReverseProxy(ctx, proxy); err != nil {
return fmt.Errorf("failed to update reverse proxy status: %w", err)
}
return nil
})
}

View File

@@ -5,6 +5,7 @@ import (
"net"
"net/url"
"strconv"
"time"
"github.com/rs/xid"
log "github.com/sirupsen/logrus"
@@ -21,6 +22,17 @@ const (
Delete Operation = "delete"
)
type ProxyStatus string
const (
StatusPending ProxyStatus = "pending"
StatusActive ProxyStatus = "active"
StatusTunnelNotCreated ProxyStatus = "tunnel_not_created"
StatusCertificatePending ProxyStatus = "certificate_pending"
StatusCertificateFailed ProxyStatus = "certificate_failed"
StatusError ProxyStatus = "error"
)
type Target struct {
Path *string `json:"path,omitempty"`
Host string `json:"host"`
@@ -64,6 +76,12 @@ type OIDCValidationConfig struct {
MaxTokenAgeSeconds int64
}
type ReverseProxyMeta struct {
CreatedAt time.Time
CertificateIssuedAt time.Time
Status string
}
type ReverseProxy struct {
ID string `gorm:"primaryKey"`
AccountID string `gorm:"index"`
@@ -71,7 +89,8 @@ type ReverseProxy struct {
Domain string `gorm:"index"`
Targets []Target `gorm:"serializer:json"`
Enabled bool
Auth AuthConfig `gorm:"serializer:json"`
Auth AuthConfig `gorm:"serializer:json"`
Meta ReverseProxyMeta `gorm:"embedded;embeddedPrefix:meta_"`
}
func NewReverseProxy(accountID, name, domain string, targets []Target, enabled bool) *ReverseProxy {
@@ -82,6 +101,10 @@ func NewReverseProxy(accountID, name, domain string, targets []Target, enabled b
Domain: domain,
Targets: targets,
Enabled: enabled,
Meta: ReverseProxyMeta{
CreatedAt: time.Now(),
Status: string(StatusPending),
},
}
}
@@ -129,6 +152,15 @@ func (r *ReverseProxy) ToAPIResponse() *api.ReverseProxy {
})
}
meta := api.ReverseProxyMeta{
CreatedAt: r.Meta.CreatedAt,
Status: api.ReverseProxyMetaStatus(r.Meta.Status),
}
if !r.Meta.CertificateIssuedAt.IsZero() {
meta.CertificateIssuedAt = &r.Meta.CertificateIssuedAt
}
return &api.ReverseProxy{
Id: r.ID,
Name: r.Name,
@@ -136,6 +168,7 @@ func (r *ReverseProxy) ToAPIResponse() *api.ReverseProxy {
Targets: apiTargets,
Enabled: r.Enabled,
Auth: authConfig,
Meta: meta,
}
}

View File

@@ -163,6 +163,9 @@ func (s *BaseServer) GRPCServer() *grpc.Server {
func (s *BaseServer) ReverseProxyGRPCServer() *nbgrpc.ProxyServiceServer {
return Create(s, func() *nbgrpc.ProxyServiceServer {
proxyService := nbgrpc.NewProxyServiceServer(s.Store(), s.AccountManager(), s.AccessLogsManager(), s.proxyOIDCConfig())
s.AfterInit(func(s *BaseServer) {
proxyService.SetProxyManager(s.ReverseProxyManager())
})
return proxyService
})
}

View File

@@ -45,6 +45,11 @@ type reverseProxyStore interface {
GetReverseProxyByID(ctx context.Context, lockStrength store.LockingStrength, accountID string, serviceID string) (*reverseproxy.ReverseProxy, error)
}
type reverseProxyManager interface {
SetCertificateIssuedAt(ctx context.Context, accountID, reverseProxyID string) error
SetStatus(ctx context.Context, accountID, reverseProxyID string, status reverseproxy.ProxyStatus) error
}
type keyStore interface {
GetGroupByName(ctx context.Context, groupName string, accountID string) (*types.Group, error)
CreateSetupKey(ctx context.Context, accountID string, keyName string, keyType types.SetupKeyType, expiresIn time.Duration, autoGroups []string, usageLimit int, userID string, ephemeral bool, allowExtraDNSLabels bool) (*types.SetupKey, error)
@@ -69,6 +74,9 @@ type ProxyServiceServer struct {
// Manager for access logs
accessLogManager accesslogs.Manager
// Manager for reverse proxy operations
reverseProxyManager reverseProxyManager
// OIDC configuration for proxy authentication
oidcConfig ProxyOIDCConfig
@@ -98,6 +106,10 @@ func NewProxyServiceServer(store reverseProxyStore, keys keyStore, accessLogMgr
}
}
func (s *ProxyServiceServer) SetProxyManager(manager reverseProxyManager) {
s.reverseProxyManager = manager
}
// GetMappingUpdate handles the control stream with proxy clients
func (s *ProxyServiceServer) GetMappingUpdate(req *proto.GetMappingUpdateRequest, stream proto.ProxyService_GetMappingUpdateServer) error {
ctx := stream.Context()
@@ -328,6 +340,72 @@ func (s *ProxyServiceServer) Authenticate(ctx context.Context, req *proto.Authen
}, nil
}
// SendStatusUpdate handles status updates from proxy clients
func (s *ProxyServiceServer) SendStatusUpdate(ctx context.Context, req *proto.SendStatusUpdateRequest) (*proto.SendStatusUpdateResponse, error) {
accountID := req.GetAccountId()
reverseProxyID := req.GetReverseProxyId()
protoStatus := req.GetStatus()
certificateIssued := req.GetCertificateIssued()
log.WithFields(log.Fields{
"reverse_proxy_id": reverseProxyID,
"account_id": accountID,
"status": protoStatus,
"certificate_issued": certificateIssued,
"error_message": req.GetErrorMessage(),
}).Debug("Status update from proxy")
if reverseProxyID == "" || accountID == "" {
return nil, status.Errorf(codes.InvalidArgument, "reverse_proxy_id and account_id are required")
}
if certificateIssued {
if err := s.reverseProxyManager.SetCertificateIssuedAt(ctx, accountID, reverseProxyID); err != nil {
log.WithContext(ctx).WithError(err).Error("Failed to set certificate issued timestamp")
return nil, status.Errorf(codes.Internal, "failed to update certificate timestamp: %v", err)
}
log.WithFields(log.Fields{
"reverse_proxy_id": reverseProxyID,
"account_id": accountID,
}).Info("Certificate issued timestamp updated")
}
internalStatus := protoStatusToInternal(protoStatus)
if err := s.reverseProxyManager.SetStatus(ctx, accountID, reverseProxyID, internalStatus); err != nil {
log.WithContext(ctx).WithError(err).Error("Failed to set proxy status")
return nil, status.Errorf(codes.Internal, "failed to update proxy status: %v", err)
}
log.WithFields(log.Fields{
"reverse_proxy_id": reverseProxyID,
"account_id": accountID,
"status": internalStatus,
}).Info("Proxy status updated")
return &proto.SendStatusUpdateResponse{}, nil
}
// protoStatusToInternal maps proto status to internal status
func protoStatusToInternal(protoStatus proto.ProxyStatus) reverseproxy.ProxyStatus {
switch protoStatus {
case proto.ProxyStatus_PROXY_STATUS_PENDING:
return reverseproxy.StatusPending
case proto.ProxyStatus_PROXY_STATUS_ACTIVE:
return reverseproxy.StatusActive
case proto.ProxyStatus_PROXY_STATUS_TUNNEL_NOT_CREATED:
return reverseproxy.StatusTunnelNotCreated
case proto.ProxyStatus_PROXY_STATUS_CERTIFICATE_PENDING:
return reverseproxy.StatusCertificatePending
case proto.ProxyStatus_PROXY_STATUS_CERTIFICATE_FAILED:
return reverseproxy.StatusCertificateFailed
case proto.ProxyStatus_PROXY_STATUS_ERROR:
return reverseproxy.StatusError
default:
return reverseproxy.StatusError
}
}
func (s *ProxyServiceServer) GetOIDCURL(ctx context.Context, req *proto.GetOIDCURLRequest) (*proto.GetOIDCURLResponse, error) {
provider, err := oidc.NewProvider(ctx, s.oidcConfig.Issuer)
if err != nil {

View File

@@ -40,6 +40,25 @@ Certificate generation can either be via ACME (by default, using Let's Encrypt,
When not using ACME, the proxy server attempts to load a certificate and key from the files `tls.crt` and `tls.key` in a specified certificate directory.
When using ACME, the proxy server will store generated certificates in the specified certificate directory.
## Auth UI
The authentication UI is a Vite + React application located in the `web/` directory. It is embedded into the Go binary at build time.
To build the UI:
```bash
cd web
npm install
npm run build
```
For UI development with hot reload (served at http://localhost:3031):
```bash
npm run dev
```
The built assets in `web/dist/` are embedded via `//go:embed` and served by the `web.ServeHTTP` handler.
## Configuration
NetBird Proxy deployment configuration is via flags or environment variables, with flags taking precedence over the environment.

View File

@@ -0,0 +1,166 @@
package cmd
import (
"fmt"
"strconv"
"github.com/spf13/cobra"
"github.com/netbirdio/netbird/proxy/internal/debug"
)
var (
debugAddr string
jsonOutput bool
// status filters
statusFilterByIPs []string
statusFilterByNames []string
statusFilterByStatus string
statusFilterByConnectionType string
)
var debugCmd = &cobra.Command{
Use: "debug",
Short: "Debug commands for inspecting proxy state",
Long: "Debug commands for inspecting the reverse proxy state via the debug HTTP endpoint.",
}
var debugHealthCmd = &cobra.Command{
Use: "health",
Short: "Show proxy health status",
RunE: runDebugHealth,
SilenceUsage: true,
}
var debugClientsCmd = &cobra.Command{
Use: "clients",
Aliases: []string{"list"},
Short: "List all connected clients",
RunE: runDebugClients,
SilenceUsage: true,
}
var debugStatusCmd = &cobra.Command{
Use: "status <account-id>",
Short: "Show client status",
Args: cobra.ExactArgs(1),
RunE: runDebugStatus,
SilenceUsage: true,
}
var debugSyncCmd = &cobra.Command{
Use: "sync-response <account-id>",
Short: "Show client sync response",
Args: cobra.ExactArgs(1),
RunE: runDebugSync,
SilenceUsage: true,
}
var pingTimeout string
var debugPingCmd = &cobra.Command{
Use: "ping <account-id> <host> [port]",
Short: "TCP ping through a client",
Long: "Perform a TCP ping through a client's network to test connectivity.\nPort defaults to 80 if not specified.",
Args: cobra.RangeArgs(2, 3),
RunE: runDebugPing,
SilenceUsage: true,
}
var debugLogLevelCmd = &cobra.Command{
Use: "loglevel <account-id> <level>",
Short: "Set client log level",
Long: "Set the log level for a client (trace, debug, info, warn, error).",
Args: cobra.ExactArgs(2),
RunE: runDebugLogLevel,
SilenceUsage: true,
}
var debugStartCmd = &cobra.Command{
Use: "start <account-id>",
Short: "Start a client",
Args: cobra.ExactArgs(1),
RunE: runDebugStart,
SilenceUsage: true,
}
var debugStopCmd = &cobra.Command{
Use: "stop <account-id>",
Short: "Stop a client",
Args: cobra.ExactArgs(1),
RunE: runDebugStop,
SilenceUsage: true,
}
func init() {
debugCmd.PersistentFlags().StringVar(&debugAddr, "addr", envStringOrDefault("NB_PROXY_DEBUG_ADDRESS", "localhost:8444"), "Debug endpoint address")
debugCmd.PersistentFlags().BoolVar(&jsonOutput, "json", false, "Output JSON instead of pretty format")
debugStatusCmd.Flags().StringSliceVar(&statusFilterByIPs, "filter-by-ips", nil, "Filter by peer IPs (comma-separated)")
debugStatusCmd.Flags().StringSliceVar(&statusFilterByNames, "filter-by-names", nil, "Filter by peer names (comma-separated)")
debugStatusCmd.Flags().StringVar(&statusFilterByStatus, "filter-by-status", "", "Filter by status (idle|connecting|connected)")
debugStatusCmd.Flags().StringVar(&statusFilterByConnectionType, "filter-by-connection-type", "", "Filter by connection type (P2P|Relayed)")
debugPingCmd.Flags().StringVar(&pingTimeout, "timeout", "", "Ping timeout (e.g., 10s)")
debugCmd.AddCommand(debugHealthCmd)
debugCmd.AddCommand(debugClientsCmd)
debugCmd.AddCommand(debugStatusCmd)
debugCmd.AddCommand(debugSyncCmd)
debugCmd.AddCommand(debugPingCmd)
debugCmd.AddCommand(debugLogLevelCmd)
debugCmd.AddCommand(debugStartCmd)
debugCmd.AddCommand(debugStopCmd)
rootCmd.AddCommand(debugCmd)
}
func getDebugClient(cmd *cobra.Command) *debug.Client {
return debug.NewClient(debugAddr, jsonOutput, cmd.OutOrStdout())
}
func runDebugHealth(cmd *cobra.Command, _ []string) error {
return getDebugClient(cmd).Health(cmd.Context())
}
func runDebugClients(cmd *cobra.Command, _ []string) error {
return getDebugClient(cmd).ListClients(cmd.Context())
}
func runDebugStatus(cmd *cobra.Command, args []string) error {
return getDebugClient(cmd).ClientStatus(cmd.Context(), args[0], debug.StatusFilters{
IPs: statusFilterByIPs,
Names: statusFilterByNames,
Status: statusFilterByStatus,
ConnectionType: statusFilterByConnectionType,
})
}
func runDebugSync(cmd *cobra.Command, args []string) error {
return getDebugClient(cmd).ClientSyncResponse(cmd.Context(), args[0])
}
func runDebugPing(cmd *cobra.Command, args []string) error {
port := 80
if len(args) > 2 {
p, err := strconv.Atoi(args[2])
if err != nil {
return fmt.Errorf("invalid port: %w", err)
}
port = p
}
return getDebugClient(cmd).PingTCP(cmd.Context(), args[0], args[1], port, pingTimeout)
}
func runDebugLogLevel(cmd *cobra.Command, args []string) error {
return getDebugClient(cmd).SetLogLevel(cmd.Context(), args[0], args[1])
}
func runDebugStart(cmd *cobra.Command, args []string) error {
return getDebugClient(cmd).StartClient(cmd.Context(), args[0])
}
func runDebugStop(cmd *cobra.Command, args []string) error {
return getDebugClient(cmd).StopClient(cmd.Context(), args[0])
}

137
proxy/cmd/proxy/cmd/root.go Normal file
View File

@@ -0,0 +1,137 @@
package cmd
import (
"context"
"os"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/crypto/acme"
"github.com/netbirdio/netbird/proxy"
"github.com/netbirdio/netbird/util"
)
const DefaultManagementURL = "https://api.netbird.io:443"
var (
Version = "dev"
Commit = "unknown"
BuildDate = "unknown"
GoVersion = "unknown"
)
var (
debugLogs bool
mgmtAddr string
addr string
proxyURL string
certDir string
acmeCerts bool
acmeAddr string
acmeDir string
debugEndpoint bool
debugEndpointAddr string
oidcClientID string
oidcClientSecret string
oidcEndpoint string
oidcScopes string
)
var rootCmd = &cobra.Command{
Use: "proxy",
Short: "NetBird reverse proxy server",
Long: "NetBird reverse proxy server for proxying traffic to NetBird networks.",
Version: Version,
RunE: runServer,
}
func init() {
rootCmd.PersistentFlags().BoolVar(&debugLogs, "debug", envBoolOrDefault("NB_PROXY_DEBUG_LOGS", false), "Enable debug logs")
rootCmd.Flags().StringVar(&mgmtAddr, "mgmt", envStringOrDefault("NB_PROXY_MANAGEMENT_ADDRESS", DefaultManagementURL), "Management address to connect to")
rootCmd.Flags().StringVar(&addr, "addr", envStringOrDefault("NB_PROXY_ADDRESS", ":443"), "Reverse proxy address to listen on")
rootCmd.Flags().StringVar(&proxyURL, "url", envStringOrDefault("NB_PROXY_URL", ""), "The URL at which this proxy will be reached")
rootCmd.Flags().StringVar(&certDir, "cert-dir", envStringOrDefault("NB_PROXY_CERTIFICATE_DIRECTORY", "./certs"), "Directory to store certificates")
rootCmd.Flags().BoolVar(&acmeCerts, "acme-certs", envBoolOrDefault("NB_PROXY_ACME_CERTIFICATES", false), "Generate ACME certificates using HTTP-01 challenges")
rootCmd.Flags().StringVar(&acmeAddr, "acme-addr", envStringOrDefault("NB_PROXY_ACME_ADDRESS", ":80"), "HTTP address for ACME HTTP-01 challenges")
rootCmd.Flags().StringVar(&acmeDir, "acme-dir", envStringOrDefault("NB_PROXY_ACME_DIRECTORY", acme.LetsEncryptURL), "URL of ACME challenge directory")
rootCmd.Flags().BoolVar(&debugEndpoint, "debug-endpoint", envBoolOrDefault("NB_PROXY_DEBUG_ENDPOINT", false), "Enable debug HTTP endpoint")
rootCmd.Flags().StringVar(&debugEndpointAddr, "debug-endpoint-addr", envStringOrDefault("NB_PROXY_DEBUG_ENDPOINT_ADDRESS", "localhost:8444"), "Address for the debug HTTP endpoint")
rootCmd.Flags().StringVar(&oidcClientID, "oidc-id", envStringOrDefault("NB_PROXY_OIDC_CLIENT_ID", "netbird-proxy"), "The OAuth2 Client ID for OIDC User Authentication")
rootCmd.Flags().StringVar(&oidcClientSecret, "oidc-secret", envStringOrDefault("NB_PROXY_OIDC_CLIENT_SECRET", ""), "The OAuth2 Client Secret for OIDC User Authentication")
rootCmd.Flags().StringVar(&oidcEndpoint, "oidc-endpoint", envStringOrDefault("NB_PROXY_OIDC_ENDPOINT", ""), "The OIDC Endpoint for OIDC User Authentication")
rootCmd.Flags().StringVar(&oidcScopes, "oidc-scopes", envStringOrDefault("NB_PROXY_OIDC_SCOPES", "openid,profile,email"), "The OAuth2 scopes for OIDC User Authentication, comma separated")
}
// Execute runs the root command.
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
// SetVersionInfo sets version information for the CLI.
func SetVersionInfo(version, commit, buildDate, goVersion string) {
Version = version
Commit = commit
BuildDate = buildDate
GoVersion = goVersion
rootCmd.Version = version
rootCmd.SetVersionTemplate("Version: {{.Version}}, Commit: " + Commit + ", BuildDate: " + BuildDate + ", Go: " + GoVersion + "\n")
}
func runServer(cmd *cobra.Command, args []string) error {
level := "error"
if debugLogs {
level = "debug"
}
logger := log.New()
_ = util.InitLogger(logger, level, util.LogConsole)
log.Infof("configured log level: %s", level)
srv := proxy.Server{
Logger: logger,
Version: Version,
ManagementAddress: mgmtAddr,
ProxyURL: proxyURL,
CertificateDirectory: certDir,
GenerateACMECertificates: acmeCerts,
ACMEChallengeAddress: acmeAddr,
ACMEDirectory: acmeDir,
DebugEndpointEnabled: debugEndpoint,
DebugEndpointAddress: debugEndpointAddr,
OIDCClientId: oidcClientID,
OIDCClientSecret: oidcClientSecret,
OIDCEndpoint: oidcEndpoint,
OIDCScopes: strings.Split(oidcScopes, ","),
}
if err := srv.ListenAndServe(context.TODO(), addr); err != nil {
log.Fatal(err)
}
return nil
}
func envBoolOrDefault(key string, def bool) bool {
v, exists := os.LookupEnv(key)
if !exists {
return def
}
parsed, err := strconv.ParseBool(v)
if err != nil {
return def
}
return parsed
}
func envStringOrDefault(key string, def string) string {
v, exists := os.LookupEnv(key)
if !exists {
return def
}
return v
}

View File

@@ -1,22 +1,11 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"runtime"
"strings"
"github.com/netbirdio/netbird/util"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/acme"
"github.com/netbirdio/netbird/proxy"
"github.com/netbirdio/netbird/proxy/cmd/proxy/cmd"
)
const DefaultManagementURL = "https://api.netbird.io:443"
var (
// Version is the application version (set via ldflags during build)
Version = "dev"
@@ -31,78 +20,7 @@ var (
GoVersion = runtime.Version()
)
func envBoolOrDefault(key string, def bool) bool {
v, exists := os.LookupEnv(key)
if !exists {
return def
}
return v == strings.ToLower("true")
}
func envStringOrDefault(key string, def string) string {
v, exists := os.LookupEnv(key)
if !exists {
return def
}
return v
}
func main() {
var (
version, debug bool
mgmtAddr, addr, url, certDir string
acmeCerts bool
acmeAddr, acmeDir string
oidcId, oidcSecret, oidcEndpoint, oidcScopes string
)
flag.BoolVar(&version, "v", false, "Print version and exit")
flag.BoolVar(&debug, "debug", envBoolOrDefault("NB_PROXY_DEBUG_LOGS", false), "Enable debug logs")
flag.StringVar(&mgmtAddr, "mgmt", envStringOrDefault("NB_PROXY_MANAGEMENT_ADDRESS", DefaultManagementURL), "Management address to connect to.")
flag.StringVar(&addr, "addr", envStringOrDefault("NB_PROXY_ADDRESS", ":443"), "Reverse proxy address to listen on.")
flag.StringVar(&url, "url", envStringOrDefault("NB_PROXY_URL", "proxy.netbird.io"), "The URL at which this proxy will be reached, where CNAME records for proxied endpoints will be directed.")
flag.StringVar(&certDir, "cert-dir", envStringOrDefault("NB_PROXY_CERTIFICATE_DIRECTORY", "./certs"), "Directory to store ")
flag.BoolVar(&acmeCerts, "acme-certs", envBoolOrDefault("NB_PROXY_ACME_CERTIFICATES", false), "Generate ACME certificates using HTTP-01 challenges.")
flag.StringVar(&acmeAddr, "acme-addr", envStringOrDefault("NB_PROXY_ACME_ADDRESS", ":80"), "HTTP address to listen on, used for ACME HTTP-01 certificate generation.")
flag.StringVar(&acmeDir, "acme-dir", envStringOrDefault("NB_PROXY_ACME_DIRECTORY", acme.LetsEncryptURL), "URL of ACME challenge directory.")
flag.StringVar(&oidcId, "oidc-id", envStringOrDefault("NB_PROXY_OIDC_CLIENT_ID", "netbird-proxy"), "The OAuth2 Client ID for OIDC User Authentication")
flag.StringVar(&oidcSecret, "oidc-secret", envStringOrDefault("NB_PROXY_OIDC_CLIENT_SECRET", ""), "The OAuth2 Client Secret for OIDC User Authentication")
flag.StringVar(&oidcEndpoint, "oidc-endpoint", envStringOrDefault("NB_PROXY_OIDC_ENDPOINT", ""), "The OIDC Endpoint for OIDC User Authentication")
flag.StringVar(&oidcScopes, "oidc-scopes", envStringOrDefault("NB_PROXY_OIDC_SCOPES", "openid,profile,email"), "The OAuth2 scopes for OIDC User Authentication, comma separated")
flag.Parse()
if version {
fmt.Printf("Version: %s, Commit: %s, BuildDate: %s, Go: %s", Version, Commit, BuildDate, GoVersion)
os.Exit(0)
}
// Configure logrus.
level := "error"
if debug {
level = "debug"
}
logger := log.New()
_ = util.InitLogger(logger, level, util.LogConsole)
log.Infof("configured log level: %s", level)
srv := proxy.Server{
Logger: logger,
Version: Version,
ManagementAddress: mgmtAddr,
ProxyURL: url,
CertificateDirectory: certDir,
GenerateACMECertificates: acmeCerts,
ACMEChallengeAddress: acmeAddr,
ACMEDirectory: acmeDir,
OIDCClientId: oidcId,
OIDCClientSecret: oidcSecret,
OIDCEndpoint: oidcEndpoint,
OIDCScopes: strings.Split(oidcScopes, ","),
}
if err := srv.ListenAndServe(context.TODO(), addr); err != nil {
log.Fatal(err)
}
cmd.SetVersionInfo(Version, Commit, BuildDate, GoVersion)
cmd.Execute()
}

View File

@@ -42,7 +42,7 @@ func (l *Logger) Middleware(next http.Handler) http.Handler {
entry := logEntry{
ID: xid.New().String(),
ServiceId: capturedData.GetServiceId(),
AccountID: capturedData.GetAccountId(),
AccountID: string(capturedData.GetAccountId()),
Host: host,
Path: r.URL.Path,
DurationMs: duration.Milliseconds(),

View File

@@ -5,20 +5,34 @@ import (
"fmt"
"sync"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
)
type certificateNotifier interface {
NotifyCertificateIssued(ctx context.Context, accountID, reverseProxyID, domain string) error
}
type Manager struct {
*autocert.Manager
domainsMux sync.RWMutex
domains map[string]struct{}
domains map[string]struct {
accountID string
reverseProxyID string
}
certNotifier certificateNotifier
}
func NewManager(certDir, acmeURL string) *Manager {
func NewManager(certDir, acmeURL string, notifier certificateNotifier) *Manager {
mgr := &Manager{
domains: make(map[string]struct{}),
domains: make(map[string]struct {
accountID string
reverseProxyID string
}),
certNotifier: notifier,
}
mgr.Manager = &autocert.Manager{
Prompt: autocert.AcceptTOS,
@@ -31,19 +45,33 @@ func NewManager(certDir, acmeURL string) *Manager {
return mgr
}
func (mgr *Manager) hostPolicy(_ context.Context, domain string) error {
func (mgr *Manager) hostPolicy(ctx context.Context, domain string) error {
mgr.domainsMux.RLock()
defer mgr.domainsMux.RUnlock()
if _, exists := mgr.domains[domain]; exists {
return nil
info, exists := mgr.domains[domain]
mgr.domainsMux.RUnlock()
if !exists {
return fmt.Errorf("unknown domain %q", domain)
}
return fmt.Errorf("unknown domain %q", domain)
if mgr.certNotifier != nil {
if err := mgr.certNotifier.NotifyCertificateIssued(ctx, info.accountID, info.reverseProxyID, domain); err != nil {
log.Warnf("failed to notify certificate issued for domain %q: %v", domain, err)
}
}
return nil
}
func (mgr *Manager) AddDomain(domain string) {
func (mgr *Manager) AddDomain(domain, accountID, reverseProxyID string) {
mgr.domainsMux.Lock()
defer mgr.domainsMux.Unlock()
mgr.domains[domain] = struct{}{}
mgr.domains[domain] = struct {
accountID string
reverseProxyID string
}{
accountID: accountID,
reverseProxyID: reverseProxyID,
}
}
func (mgr *Manager) RemoveDomain(domain string) {

View File

@@ -3,9 +3,7 @@ package auth
import (
"context"
"crypto/rand"
_ "embed"
"encoding/base64"
"html/template"
"net"
"net/http"
"sync"
@@ -13,12 +11,10 @@ import (
"google.golang.org/grpc"
"github.com/netbirdio/netbird/proxy/web"
"github.com/netbirdio/netbird/shared/management/proto"
)
//go:embed auth.gohtml
var authTemplate string
type Method string
var (
@@ -85,7 +81,6 @@ func NewMiddleware() *Middleware {
// In the event that no authentication schemes are defined for the domain,
// then the request will also be simply passed through.
func (mw *Middleware) Protect(next http.Handler) http.Handler {
tmpl := template.Must(template.New("auth").Parse(authTemplate))
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host, _, err := net.SplitHostPort(r.Host)
if err != nil {
@@ -131,13 +126,7 @@ func (mw *Middleware) Protect(next http.Handler) http.Handler {
methods[s.Type().String()] = promptData
}
if err := tmpl.Execute(w, struct {
Methods map[string]string
}{
Methods: methods,
}); err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
}
web.ServeHTTP(w, r, map[string]any{"methods": methods})
})
}

View File

@@ -0,0 +1,307 @@
// Package debug provides HTTP debug endpoints and CLI client for the proxy server.
package debug
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// StatusFilters contains filter options for status queries.
type StatusFilters struct {
IPs []string
Names []string
Status string
ConnectionType string
}
// Client provides CLI access to debug endpoints.
type Client struct {
baseURL string
jsonOutput bool
httpClient *http.Client
out io.Writer
}
// NewClient creates a new debug client.
func NewClient(baseURL string, jsonOutput bool, out io.Writer) *Client {
if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
baseURL = "http://" + baseURL
}
baseURL = strings.TrimSuffix(baseURL, "/")
return &Client{
baseURL: baseURL,
jsonOutput: jsonOutput,
out: out,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
}
}
// Health fetches the health status.
func (c *Client) Health(ctx context.Context) error {
return c.fetchAndPrint(ctx, "/debug/health", c.printHealth)
}
func (c *Client) printHealth(data map[string]any) {
_, _ = fmt.Fprintf(c.out, "Status: %v\n", data["status"])
_, _ = fmt.Fprintf(c.out, "Uptime: %v\n", data["uptime"])
}
// ListClients fetches the list of all clients.
func (c *Client) ListClients(ctx context.Context) error {
return c.fetchAndPrint(ctx, "/debug/clients", c.printClients)
}
func (c *Client) printClients(data map[string]any) {
_, _ = fmt.Fprintf(c.out, "Uptime: %v\n", data["uptime"])
_, _ = fmt.Fprintf(c.out, "Clients: %v\n\n", data["client_count"])
clients, ok := data["clients"].([]any)
if !ok || len(clients) == 0 {
_, _ = fmt.Fprintln(c.out, "No clients connected.")
return
}
_, _ = fmt.Fprintf(c.out, "%-38s %-12s %-40s %s\n", "ACCOUNT ID", "AGE", "DOMAINS", "HAS CLIENT")
_, _ = fmt.Fprintln(c.out, strings.Repeat("-", 110))
for _, item := range clients {
c.printClientRow(item)
}
}
func (c *Client) printClientRow(item any) {
client, ok := item.(map[string]any)
if !ok {
return
}
domains := c.extractDomains(client)
hasClient := "no"
if hc, ok := client["has_client"].(bool); ok && hc {
hasClient = "yes"
}
_, _ = fmt.Fprintf(c.out, "%-38s %-12v %s %s\n",
client["account_id"],
client["age"],
domains,
hasClient,
)
}
func (c *Client) extractDomains(client map[string]any) string {
d, ok := client["domains"].([]any)
if !ok || len(d) == 0 {
return "-"
}
parts := make([]string, len(d))
for i, domain := range d {
parts[i] = fmt.Sprint(domain)
}
return strings.Join(parts, ", ")
}
// ClientStatus fetches the status of a specific client.
func (c *Client) ClientStatus(ctx context.Context, accountID string, filters StatusFilters) error {
params := url.Values{}
if len(filters.IPs) > 0 {
params.Set("filter-by-ips", strings.Join(filters.IPs, ","))
}
if len(filters.Names) > 0 {
params.Set("filter-by-names", strings.Join(filters.Names, ","))
}
if filters.Status != "" {
params.Set("filter-by-status", filters.Status)
}
if filters.ConnectionType != "" {
params.Set("filter-by-connection-type", filters.ConnectionType)
}
path := "/debug/clients/" + url.PathEscape(accountID)
if len(params) > 0 {
path += "?" + params.Encode()
}
return c.fetchAndPrint(ctx, path, c.printClientStatus)
}
func (c *Client) printClientStatus(data map[string]any) {
_, _ = fmt.Fprintf(c.out, "Account: %v\n\n", data["account_id"])
if status, ok := data["status"].(string); ok {
_, _ = fmt.Fprint(c.out, status)
}
}
// ClientSyncResponse fetches the sync response of a specific client.
func (c *Client) ClientSyncResponse(ctx context.Context, accountID string) error {
path := "/debug/clients/" + url.PathEscape(accountID) + "/syncresponse"
return c.fetchAndPrintJSON(ctx, path)
}
// PingTCP performs a TCP ping through a client.
func (c *Client) PingTCP(ctx context.Context, accountID, host string, port int, timeout string) error {
params := url.Values{}
params.Set("host", host)
params.Set("port", fmt.Sprintf("%d", port))
if timeout != "" {
params.Set("timeout", timeout)
}
path := fmt.Sprintf("/debug/clients/%s/pingtcp?%s", url.PathEscape(accountID), params.Encode())
return c.fetchAndPrint(ctx, path, c.printPingResult)
}
func (c *Client) printPingResult(data map[string]any) {
success, _ := data["success"].(bool)
if success {
_, _ = fmt.Fprintf(c.out, "Success: %v:%v\n", data["host"], data["port"])
_, _ = fmt.Fprintf(c.out, "Latency: %v\n", data["latency"])
} else {
_, _ = fmt.Fprintf(c.out, "Failed: %v:%v\n", data["host"], data["port"])
c.printError(data)
}
}
// SetLogLevel sets the log level of a specific client.
func (c *Client) SetLogLevel(ctx context.Context, accountID, level string) error {
params := url.Values{}
params.Set("level", level)
path := fmt.Sprintf("/debug/clients/%s/loglevel?%s", url.PathEscape(accountID), params.Encode())
return c.fetchAndPrint(ctx, path, c.printLogLevelResult)
}
func (c *Client) printLogLevelResult(data map[string]any) {
success, _ := data["success"].(bool)
if success {
_, _ = fmt.Fprintf(c.out, "Log level set to: %v\n", data["level"])
} else {
_, _ = fmt.Fprintln(c.out, "Failed to set log level")
c.printError(data)
}
}
// StartClient starts a specific client.
func (c *Client) StartClient(ctx context.Context, accountID string) error {
path := "/debug/clients/" + url.PathEscape(accountID) + "/start"
return c.fetchAndPrint(ctx, path, c.printStartResult)
}
func (c *Client) printStartResult(data map[string]any) {
success, _ := data["success"].(bool)
if success {
_, _ = fmt.Fprintln(c.out, "Client started")
} else {
_, _ = fmt.Fprintln(c.out, "Failed to start client")
c.printError(data)
}
}
// StopClient stops a specific client.
func (c *Client) StopClient(ctx context.Context, accountID string) error {
path := "/debug/clients/" + url.PathEscape(accountID) + "/stop"
return c.fetchAndPrint(ctx, path, c.printStopResult)
}
func (c *Client) printStopResult(data map[string]any) {
success, _ := data["success"].(bool)
if success {
_, _ = fmt.Fprintln(c.out, "Client stopped")
} else {
_, _ = fmt.Fprintln(c.out, "Failed to stop client")
c.printError(data)
}
}
func (c *Client) printError(data map[string]any) {
if errMsg, ok := data["error"].(string); ok {
_, _ = fmt.Fprintf(c.out, "Error: %s\n", errMsg)
}
}
func (c *Client) fetchAndPrint(ctx context.Context, path string, printer func(map[string]any)) error {
data, raw, err := c.fetch(ctx, path)
if err != nil {
return err
}
if c.jsonOutput {
return c.writeJSON(data)
}
if data != nil {
printer(data)
return nil
}
_, _ = fmt.Fprintln(c.out, string(raw))
return nil
}
func (c *Client) fetchAndPrintJSON(ctx context.Context, path string) error {
data, raw, err := c.fetch(ctx, path)
if err != nil {
return err
}
if data != nil {
return c.writeJSON(data)
}
_, _ = fmt.Fprintln(c.out, string(raw))
return nil
}
func (c *Client) writeJSON(data map[string]any) error {
enc := json.NewEncoder(c.out)
enc.SetIndent("", " ")
return enc.Encode(data)
}
func (c *Client) fetch(ctx context.Context, path string) (map[string]any, []byte, error) {
fullURL := c.baseURL + path
if !strings.Contains(path, "format=json") {
if strings.Contains(path, "?") {
fullURL += "&format=json"
} else {
fullURL += "?format=json"
}
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil)
if err != nil {
return nil, nil, fmt.Errorf("create request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, nil, fmt.Errorf("read response: %w", err)
}
if resp.StatusCode >= 400 {
return nil, nil, fmt.Errorf("server error (%d): %s", resp.StatusCode, strings.TrimSpace(string(body)))
}
var data map[string]any
if err := json.Unmarshal(body, &data); err != nil {
return nil, body, nil
}
return data, body, nil
}

View File

@@ -0,0 +1,589 @@
// Package debug provides HTTP debug endpoints for the proxy server.
package debug
import (
"context"
"embed"
"encoding/json"
"fmt"
"html/template"
"net/http"
"strconv"
"strings"
"sync"
"time"
log "github.com/sirupsen/logrus"
"google.golang.org/protobuf/encoding/protojson"
nbembed "github.com/netbirdio/netbird/client/embed"
nbstatus "github.com/netbirdio/netbird/client/status"
"github.com/netbirdio/netbird/proxy/internal/roundtrip"
"github.com/netbirdio/netbird/proxy/internal/types"
"github.com/netbirdio/netbird/version"
)
//go:embed templates/*.html
var templateFS embed.FS
const defaultPingTimeout = 10 * time.Second
// formatDuration formats a duration with 2 decimal places using appropriate units.
func formatDuration(d time.Duration) string {
switch {
case d >= time.Hour:
return fmt.Sprintf("%.2fh", d.Hours())
case d >= time.Minute:
return fmt.Sprintf("%.2fm", d.Minutes())
case d >= time.Second:
return fmt.Sprintf("%.2fs", d.Seconds())
case d >= time.Millisecond:
return fmt.Sprintf("%.2fms", float64(d.Microseconds())/1000)
case d >= time.Microsecond:
return fmt.Sprintf("%.2fµs", float64(d.Nanoseconds())/1000)
default:
return fmt.Sprintf("%dns", d.Nanoseconds())
}
}
// ClientProvider provides access to NetBird clients.
type ClientProvider interface {
GetClient(accountID types.AccountID) (*nbembed.Client, bool)
ListClientsForDebug() map[types.AccountID]roundtrip.ClientDebugInfo
}
// Handler provides HTTP debug endpoints.
type Handler struct {
provider ClientProvider
logger *log.Logger
startTime time.Time
templates *template.Template
templateMu sync.RWMutex
}
// NewHandler creates a new debug handler.
func NewHandler(provider ClientProvider, logger *log.Logger) *Handler {
if logger == nil {
logger = log.StandardLogger()
}
h := &Handler{
provider: provider,
logger: logger,
startTime: time.Now(),
}
if err := h.loadTemplates(); err != nil {
logger.Errorf("failed to load embedded templates: %v", err)
}
return h
}
func (h *Handler) loadTemplates() error {
tmpl, err := template.ParseFS(templateFS, "templates/*.html")
if err != nil {
return fmt.Errorf("parse embedded templates: %w", err)
}
h.templateMu.Lock()
h.templates = tmpl
h.templateMu.Unlock()
return nil
}
func (h *Handler) getTemplates() *template.Template {
h.templateMu.RLock()
defer h.templateMu.RUnlock()
return h.templates
}
// ServeHTTP handles debug requests.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
wantJSON := r.URL.Query().Get("format") == "json" || strings.HasSuffix(path, "/json")
path = strings.TrimSuffix(path, "/json")
switch path {
case "/debug", "/debug/":
h.handleIndex(w, r, wantJSON)
case "/debug/clients":
h.handleListClients(w, r, wantJSON)
case "/debug/health":
h.handleHealth(w, r, wantJSON)
default:
if h.handleClientRoutes(w, r, path, wantJSON) {
return
}
http.NotFound(w, r)
}
}
func (h *Handler) handleClientRoutes(w http.ResponseWriter, r *http.Request, path string, wantJSON bool) bool {
if !strings.HasPrefix(path, "/debug/clients/") {
return false
}
rest := strings.TrimPrefix(path, "/debug/clients/")
parts := strings.SplitN(rest, "/", 2)
accountID := types.AccountID(parts[0])
if len(parts) == 1 {
h.handleClientStatus(w, r, accountID, wantJSON)
return true
}
switch parts[1] {
case "syncresponse":
h.handleClientSyncResponse(w, r, accountID, wantJSON)
case "tools":
h.handleClientTools(w, r, accountID)
case "pingtcp":
h.handlePingTCP(w, r, accountID)
case "loglevel":
h.handleLogLevel(w, r, accountID)
case "start":
h.handleClientStart(w, r, accountID)
case "stop":
h.handleClientStop(w, r, accountID)
default:
return false
}
return true
}
type indexData struct {
Version string
Uptime string
ClientCount int
TotalDomains int
Clients []clientData
}
type clientData struct {
AccountID string
Domains string
Age string
Status string
}
func (h *Handler) handleIndex(w http.ResponseWriter, _ *http.Request, wantJSON bool) {
clients := h.provider.ListClientsForDebug()
totalDomains := 0
for _, info := range clients {
totalDomains += info.DomainCount
}
if wantJSON {
clientsJSON := make([]map[string]interface{}, 0, len(clients))
for _, info := range clients {
clientsJSON = append(clientsJSON, map[string]interface{}{
"account_id": info.AccountID,
"domain_count": info.DomainCount,
"domains": info.Domains,
"has_client": info.HasClient,
"created_at": info.CreatedAt,
"age": time.Since(info.CreatedAt).Round(time.Second).String(),
})
}
h.writeJSON(w, map[string]interface{}{
"version": version.NetbirdVersion(),
"uptime": time.Since(h.startTime).Round(time.Second).String(),
"client_count": len(clients),
"total_domains": totalDomains,
"clients": clientsJSON,
})
return
}
data := indexData{
Version: version.NetbirdVersion(),
Uptime: time.Since(h.startTime).Round(time.Second).String(),
ClientCount: len(clients),
TotalDomains: totalDomains,
Clients: make([]clientData, 0, len(clients)),
}
for _, info := range clients {
domains := info.Domains.SafeString()
if domains == "" {
domains = "-"
}
status := "No client"
if info.HasClient {
status = "Active"
}
data.Clients = append(data.Clients, clientData{
AccountID: string(info.AccountID),
Domains: domains,
Age: time.Since(info.CreatedAt).Round(time.Second).String(),
Status: status,
})
}
h.renderTemplate(w, "index", data)
}
type clientsData struct {
Uptime string
Clients []clientData
}
func (h *Handler) handleListClients(w http.ResponseWriter, _ *http.Request, wantJSON bool) {
clients := h.provider.ListClientsForDebug()
if wantJSON {
clientsJSON := make([]map[string]interface{}, 0, len(clients))
for _, info := range clients {
clientsJSON = append(clientsJSON, map[string]interface{}{
"account_id": info.AccountID,
"domain_count": info.DomainCount,
"domains": info.Domains,
"has_client": info.HasClient,
"created_at": info.CreatedAt,
"age": time.Since(info.CreatedAt).Round(time.Second).String(),
})
}
h.writeJSON(w, map[string]interface{}{
"uptime": time.Since(h.startTime).Round(time.Second).String(),
"client_count": len(clients),
"clients": clientsJSON,
})
return
}
data := clientsData{
Uptime: time.Since(h.startTime).Round(time.Second).String(),
Clients: make([]clientData, 0, len(clients)),
}
for _, info := range clients {
domains := info.Domains.SafeString()
if domains == "" {
domains = "-"
}
status := "No client"
if info.HasClient {
status = "Active"
}
data.Clients = append(data.Clients, clientData{
AccountID: string(info.AccountID),
Domains: domains,
Age: time.Since(info.CreatedAt).Round(time.Second).String(),
Status: status,
})
}
h.renderTemplate(w, "clients", data)
}
type clientDetailData struct {
AccountID string
ActiveTab string
Content string
}
func (h *Handler) handleClientStatus(w http.ResponseWriter, r *http.Request, accountID types.AccountID, wantJSON bool) {
client, ok := h.provider.GetClient(accountID)
if !ok {
http.Error(w, "Client not found: "+string(accountID), http.StatusNotFound)
return
}
fullStatus, err := client.Status()
if err != nil {
http.Error(w, "Error getting status: "+err.Error(), http.StatusInternalServerError)
return
}
// Parse filter parameters
query := r.URL.Query()
statusFilter := query.Get("filter-by-status")
connectionTypeFilter := query.Get("filter-by-connection-type")
var prefixNamesFilter []string
var prefixNamesFilterMap map[string]struct{}
if names := query.Get("filter-by-names"); names != "" {
prefixNamesFilter = strings.Split(names, ",")
prefixNamesFilterMap = make(map[string]struct{})
for _, name := range prefixNamesFilter {
prefixNamesFilterMap[strings.ToLower(strings.TrimSpace(name))] = struct{}{}
}
}
var ipsFilterMap map[string]struct{}
if ips := query.Get("filter-by-ips"); ips != "" {
ipsFilterMap = make(map[string]struct{})
for _, ip := range strings.Split(ips, ",") {
ipsFilterMap[strings.TrimSpace(ip)] = struct{}{}
}
}
pbStatus := nbstatus.ToProtoFullStatus(fullStatus)
overview := nbstatus.ConvertToStatusOutputOverview(
pbStatus,
false,
version.NetbirdVersion(),
statusFilter,
prefixNamesFilter,
prefixNamesFilterMap,
ipsFilterMap,
connectionTypeFilter,
"",
)
if wantJSON {
h.writeJSON(w, map[string]interface{}{
"account_id": accountID,
"status": overview.FullDetailSummary(),
})
return
}
data := clientDetailData{
AccountID: string(accountID),
ActiveTab: "status",
Content: overview.FullDetailSummary(),
}
h.renderTemplate(w, "clientDetail", data)
}
func (h *Handler) handleClientSyncResponse(w http.ResponseWriter, _ *http.Request, accountID types.AccountID, wantJSON bool) {
client, ok := h.provider.GetClient(accountID)
if !ok {
http.Error(w, "Client not found: "+string(accountID), http.StatusNotFound)
return
}
syncResp, err := client.GetLatestSyncResponse()
if err != nil {
http.Error(w, "Error getting sync response: "+err.Error(), http.StatusInternalServerError)
return
}
if syncResp == nil {
http.Error(w, "No sync response available for client: "+string(accountID), http.StatusNotFound)
return
}
opts := protojson.MarshalOptions{
EmitUnpopulated: true,
UseProtoNames: true,
Indent: " ",
AllowPartial: true,
}
jsonBytes, err := opts.Marshal(syncResp)
if err != nil {
http.Error(w, "Error marshaling sync response: "+err.Error(), http.StatusInternalServerError)
return
}
if wantJSON {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(jsonBytes)
return
}
data := clientDetailData{
AccountID: string(accountID),
ActiveTab: "syncresponse",
Content: string(jsonBytes),
}
h.renderTemplate(w, "clientDetail", data)
}
type toolsData struct {
AccountID string
}
func (h *Handler) handleClientTools(w http.ResponseWriter, _ *http.Request, accountID types.AccountID) {
_, ok := h.provider.GetClient(accountID)
if !ok {
http.Error(w, "Client not found: "+string(accountID), http.StatusNotFound)
return
}
data := toolsData{
AccountID: string(accountID),
}
h.renderTemplate(w, "tools", data)
}
func (h *Handler) handlePingTCP(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {
client, ok := h.provider.GetClient(accountID)
if !ok {
h.writeJSON(w, map[string]interface{}{"error": "client not found"})
return
}
host := r.URL.Query().Get("host")
portStr := r.URL.Query().Get("port")
if host == "" || portStr == "" {
h.writeJSON(w, map[string]interface{}{"error": "host and port parameters required"})
return
}
port, err := strconv.Atoi(portStr)
if err != nil || port < 1 || port > 65535 {
h.writeJSON(w, map[string]interface{}{"error": "invalid port"})
return
}
timeout := defaultPingTimeout
if t := r.URL.Query().Get("timeout"); t != "" {
if d, err := time.ParseDuration(t); err == nil {
timeout = d
}
}
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
address := fmt.Sprintf("%s:%d", host, port)
start := time.Now()
conn, err := client.Dial(ctx, "tcp", address)
if err != nil {
h.writeJSON(w, map[string]interface{}{
"success": false,
"host": host,
"port": port,
"error": err.Error(),
})
return
}
if err := conn.Close(); err != nil {
h.logger.Debugf("close tcp ping connection: %v", err)
}
latency := time.Since(start)
h.writeJSON(w, map[string]interface{}{
"success": true,
"host": host,
"port": port,
"latency_ms": latency.Milliseconds(),
"latency": formatDuration(latency),
})
}
func (h *Handler) handleLogLevel(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {
client, ok := h.provider.GetClient(accountID)
if !ok {
h.writeJSON(w, map[string]interface{}{"error": "client not found"})
return
}
level := r.URL.Query().Get("level")
if level == "" {
h.writeJSON(w, map[string]interface{}{"error": "level parameter required (trace, debug, info, warn, error)"})
return
}
if err := client.SetLogLevel(level); err != nil {
h.writeJSON(w, map[string]interface{}{
"success": false,
"error": err.Error(),
})
return
}
h.writeJSON(w, map[string]interface{}{
"success": true,
"level": level,
})
}
const clientActionTimeout = 30 * time.Second
func (h *Handler) handleClientStart(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {
client, ok := h.provider.GetClient(accountID)
if !ok {
h.writeJSON(w, map[string]interface{}{"error": "client not found"})
return
}
ctx, cancel := context.WithTimeout(r.Context(), clientActionTimeout)
defer cancel()
if err := client.Start(ctx); err != nil {
h.writeJSON(w, map[string]interface{}{
"success": false,
"error": err.Error(),
})
return
}
h.writeJSON(w, map[string]interface{}{
"success": true,
"message": "client started",
})
}
func (h *Handler) handleClientStop(w http.ResponseWriter, r *http.Request, accountID types.AccountID) {
client, ok := h.provider.GetClient(accountID)
if !ok {
h.writeJSON(w, map[string]interface{}{"error": "client not found"})
return
}
ctx, cancel := context.WithTimeout(r.Context(), clientActionTimeout)
defer cancel()
if err := client.Stop(ctx); err != nil {
h.writeJSON(w, map[string]interface{}{
"success": false,
"error": err.Error(),
})
return
}
h.writeJSON(w, map[string]interface{}{
"success": true,
"message": "client stopped",
})
}
type healthData struct {
Uptime string
}
func (h *Handler) handleHealth(w http.ResponseWriter, _ *http.Request, wantJSON bool) {
if wantJSON {
h.writeJSON(w, map[string]interface{}{
"status": "ok",
"uptime": time.Since(h.startTime).Round(10 * time.Millisecond).String(),
})
return
}
data := healthData{
Uptime: time.Since(h.startTime).Round(time.Second).String(),
}
h.renderTemplate(w, "health", data)
}
func (h *Handler) renderTemplate(w http.ResponseWriter, name string, data interface{}) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl := h.getTemplates()
if tmpl == nil {
http.Error(w, "Templates not loaded", http.StatusInternalServerError)
return
}
if err := tmpl.ExecuteTemplate(w, name, data); err != nil {
h.logger.Errorf("execute template %s: %v", name, err)
http.Error(w, "Template error", http.StatusInternalServerError)
}
}
func (h *Handler) writeJSON(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err := enc.Encode(v); err != nil {
h.logger.Errorf("encode JSON response: %v", err)
}
}

View File

@@ -0,0 +1,101 @@
{{define "style"}}
body {
font-family: monospace;
margin: 20px;
background: #1a1a1a;
color: #eee;
}
a {
color: #6cf;
}
h1, h2, h3 {
color: #fff;
}
.info {
color: #aaa;
}
table {
border-collapse: collapse;
margin: 10px 0;
}
th, td {
border: 1px solid #444;
padding: 8px;
text-align: left;
}
th {
background: #333;
}
.nav {
margin-bottom: 20px;
}
.nav a {
margin-right: 15px;
padding: 8px 16px;
background: #333;
text-decoration: none;
border-radius: 4px;
}
.nav a.active {
background: #6cf;
color: #000;
}
pre {
background: #222;
padding: 15px;
border-radius: 4px;
overflow-x: auto;
white-space: pre-wrap;
}
input, select, textarea {
background: #333;
color: #eee;
border: 1px solid #555;
padding: 8px;
border-radius: 4px;
font-family: monospace;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #6cf;
}
button {
background: #6cf;
color: #000;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-family: monospace;
}
button:hover {
background: #5be;
}
button:disabled {
background: #555;
color: #888;
cursor: not-allowed;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #aaa;
}
.form-row {
display: flex;
gap: 10px;
align-items: flex-end;
}
.result {
margin-top: 20px;
}
.success {
color: #5f5;
}
.error {
color: #f55;
}
{{end}}

View File

@@ -0,0 +1,19 @@
{{define "clientDetail"}}
<!DOCTYPE html>
<html>
<head>
<title>Client {{.AccountID}}</title>
<style>{{template "style"}}</style>
</head>
<body>
<h1>Client: {{.AccountID}}</h1>
<div class="nav">
<a href="/debug">&larr; Back</a>
<a href="/debug/clients/{{.AccountID}}/tools"{{if eq .ActiveTab "tools"}} class="active"{{end}}>Tools</a>
<a href="/debug/clients/{{.AccountID}}"{{if eq .ActiveTab "status"}} class="active"{{end}}>Status</a>
<a href="/debug/clients/{{.AccountID}}/syncresponse"{{if eq .ActiveTab "syncresponse"}} class="active"{{end}}>Sync Response</a>
</div>
<pre>{{.Content}}</pre>
</body>
</html>
{{end}}

View File

@@ -0,0 +1,33 @@
{{define "clients"}}
<!DOCTYPE html>
<html>
<head>
<title>Clients</title>
<style>{{template "style"}}</style>
</head>
<body>
<h1>All Clients</h1>
<p class="info">Uptime: {{.Uptime}} | <a href="/debug">&larr; Back</a></p>
{{if .Clients}}
<table>
<tr>
<th>Account ID</th>
<th>Domains</th>
<th>Age</th>
<th>Status</th>
</tr>
{{range .Clients}}
<tr>
<td><a href="/debug/clients/{{.AccountID}}/tools">{{.AccountID}}</a></td>
<td>{{.Domains}}</td>
<td>{{.Age}}</td>
<td>{{.Status}}</td>
</tr>
{{end}}
</table>
{{else}}
<p>No clients connected</p>
{{end}}
</body>
</html>
{{end}}

View File

@@ -0,0 +1,14 @@
{{define "health"}}
<!DOCTYPE html>
<html>
<head>
<title>Health</title>
<style>{{template "style"}}</style>
</head>
<body>
<h1>OK</h1>
<p>Uptime: {{.Uptime}}</p>
<p><a href="/debug">&larr; Back</a></p>
</body>
</html>
{{end}}

View File

@@ -0,0 +1,40 @@
{{define "index"}}
<!DOCTYPE html>
<html>
<head>
<title>NetBird Proxy Debug</title>
<style>{{template "style"}}</style>
</head>
<body>
<h1>NetBird Proxy Debug</h1>
<p class="info">Version: {{.Version}} | Uptime: {{.Uptime}}</p>
<h2>Clients ({{.ClientCount}}) | Domains ({{.TotalDomains}})</h2>
{{if .Clients}}
<table>
<tr>
<th>Account ID</th>
<th>Domains</th>
<th>Age</th>
<th>Status</th>
</tr>
{{range .Clients}}
<tr>
<td><a href="/debug/clients/{{.AccountID}}/tools">{{.AccountID}}</a></td>
<td>{{.Domains}}</td>
<td>{{.Age}}</td>
<td>{{.Status}}</td>
</tr>
{{end}}
</table>
{{else}}
<p>No clients connected</p>
{{end}}
<h2>Endpoints</h2>
<ul>
<li><a href="/debug/clients">/debug/clients</a> - all clients detail</li>
<li><a href="/debug/health">/debug/health</a> - health check</li>
</ul>
<p class="info">Add ?format=json or /json suffix for JSON output</p>
</body>
</html>
{{end}}

View File

@@ -0,0 +1,142 @@
{{define "tools"}}
<!DOCTYPE html>
<html>
<head>
<title>Client {{.AccountID}} - Tools</title>
<style>{{template "style"}}</style>
</head>
<body>
<h1>Client: {{.AccountID}}</h1>
<div class="nav">
<a href="/debug">&larr; Back</a>
<a href="/debug/clients/{{.AccountID}}/tools" class="active">Tools</a>
<a href="/debug/clients/{{.AccountID}}">Status</a>
<a href="/debug/clients/{{.AccountID}}/syncresponse">Sync Response</a>
</div>
<h2>Client Control</h2>
<div class="form-row">
<div class="form-group">
<label>&nbsp;</label>
<button onclick="startClient()">Start</button>
</div>
<div class="form-group">
<label>&nbsp;</label>
<button onclick="stopClient()">Stop</button>
</div>
</div>
<div id="client-result" class="result"></div>
<h2>Log Level</h2>
<div class="form-row">
<div class="form-group">
<label>Level</label>
<select id="log-level" style="width: 120px;">
<option value="trace">trace</option>
<option value="debug">debug</option>
<option value="info">info</option>
<option value="warn" selected>warn</option>
<option value="error">error</option>
</select>
</div>
<div class="form-group">
<label>&nbsp;</label>
<button onclick="setLogLevel()">Set Level</button>
</div>
</div>
<div id="log-result" class="result"></div>
<h2>TCP Ping</h2>
<div class="form-row">
<div class="form-group">
<label>Host</label>
<input type="text" id="tcp-host" placeholder="100.0.0.1 or hostname.netbird.cloud" style="width: 300px;">
</div>
<div class="form-group">
<label>Port</label>
<input type="number" id="tcp-port" placeholder="80" style="width: 80px;">
</div>
<div class="form-group">
<label>&nbsp;</label>
<button onclick="doTcpPing()">Connect</button>
</div>
</div>
<div id="tcp-result" class="result"></div>
<script>
const accountID = "{{.AccountID}}";
async function startClient() {
const resultDiv = document.getElementById('client-result');
resultDiv.innerHTML = '<span class="info">Starting client...</span>';
try {
const resp = await fetch('/debug/clients/' + accountID + '/start');
const data = await resp.json();
if (data.success) {
resultDiv.innerHTML = '<span class="success">✓ ' + data.message + '</span>';
} else {
resultDiv.innerHTML = '<span class="error">✗ ' + data.error + '</span>';
}
} catch (e) {
resultDiv.innerHTML = '<span class="error">Error: ' + e.message + '</span>';
}
}
async function stopClient() {
const resultDiv = document.getElementById('client-result');
resultDiv.innerHTML = '<span class="info">Stopping client...</span>';
try {
const resp = await fetch('/debug/clients/' + accountID + '/stop');
const data = await resp.json();
if (data.success) {
resultDiv.innerHTML = '<span class="success">✓ ' + data.message + '</span>';
} else {
resultDiv.innerHTML = '<span class="error">✗ ' + data.error + '</span>';
}
} catch (e) {
resultDiv.innerHTML = '<span class="error">Error: ' + e.message + '</span>';
}
}
async function setLogLevel() {
const level = document.getElementById('log-level').value;
const resultDiv = document.getElementById('log-result');
resultDiv.innerHTML = '<span class="info">Setting log level...</span>';
try {
const resp = await fetch('/debug/clients/' + accountID + '/loglevel?level=' + level);
const data = await resp.json();
if (data.success) {
resultDiv.innerHTML = '<span class="success">✓ Log level set to: ' + data.level + '</span>';
} else {
resultDiv.innerHTML = '<span class="error">✗ ' + data.error + '</span>';
}
} catch (e) {
resultDiv.innerHTML = '<span class="error">Error: ' + e.message + '</span>';
}
}
async function doTcpPing() {
const host = document.getElementById('tcp-host').value;
const port = document.getElementById('tcp-port').value;
if (!host || !port) {
alert('Host and port required');
return;
}
const resultDiv = document.getElementById('tcp-result');
resultDiv.innerHTML = '<span class="info">Connecting...</span>';
try {
const resp = await fetch('/debug/clients/' + accountID + '/pingtcp?host=' + encodeURIComponent(host) + '&port=' + port);
const data = await resp.json();
if (data.success) {
resultDiv.innerHTML = '<span class="success">✓ ' + data.host + ':' + data.port + ' connected in ' + data.latency + '</span>';
} else {
resultDiv.innerHTML = '<span class="error">✗ ' + data.host + ':' + data.port + ': ' + data.error + '</span>';
}
} catch (e) {
resultDiv.innerHTML = '<span class="error">Error: ' + e.message + '</span>';
}
}
</script>
</body>
</html>
{{end}}

View File

@@ -3,6 +3,8 @@ package proxy
import (
"context"
"sync"
"github.com/netbirdio/netbird/proxy/internal/types"
)
type requestContextKey string
@@ -18,7 +20,7 @@ const (
type CapturedData struct {
mu sync.RWMutex
ServiceId string
AccountId string
AccountId types.AccountID
}
// SetServiceId safely sets the service ID
@@ -36,14 +38,14 @@ func (c *CapturedData) GetServiceId() string {
}
// SetAccountId safely sets the account ID
func (c *CapturedData) SetAccountId(accountId string) {
func (c *CapturedData) SetAccountId(accountId types.AccountID) {
c.mu.Lock()
defer c.mu.Unlock()
c.AccountId = accountId
}
// GetAccountId safely gets the account ID
func (c *CapturedData) GetAccountId() string {
func (c *CapturedData) GetAccountId() types.AccountID {
c.mu.RLock()
defer c.mu.RUnlock()
return c.AccountId
@@ -76,13 +78,13 @@ func ServiceIdFromContext(ctx context.Context) string {
}
return serviceId
}
func withAccountId(ctx context.Context, accountId string) context.Context {
func withAccountId(ctx context.Context, accountId types.AccountID) context.Context {
return context.WithValue(ctx, accountIdKey, accountId)
}
func AccountIdFromContext(ctx context.Context) string {
func AccountIdFromContext(ctx context.Context) types.AccountID {
v := ctx.Value(accountIdKey)
accountId, ok := v.(string)
accountId, ok := v.(types.AccountID)
if !ok {
return ""
}

View File

@@ -4,6 +4,8 @@ import (
"net/http"
"net/http/httputil"
"sync"
"github.com/netbirdio/netbird/proxy/internal/roundtrip"
)
type ReverseProxy struct {
@@ -36,8 +38,10 @@ func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Set the serviceId in the context for later retrieval.
ctx := withServiceId(r.Context(), serviceId)
// Set the accountId in the context for later retrieval.
// Set the accountId in the context for later retrieval (for middleware).
ctx = withAccountId(ctx, accountID)
// Set the accountId in the context for the roundtripper to use.
ctx = roundtrip.WithAccountID(ctx, accountID)
// Also populate captured data if it exists (allows middleware to read after handler completes).
// This solves the problem of passing data UP the middleware chain: we put a mutable struct

View File

@@ -6,16 +6,18 @@ import (
"net/url"
"sort"
"strings"
"github.com/netbirdio/netbird/proxy/internal/types"
)
type Mapping struct {
ID string
AccountID string
AccountID types.AccountID
Host string
Paths map[string]*url.URL
}
func (p *ReverseProxy) findTargetForRequest(req *http.Request) (*url.URL, string, string, bool) {
func (p *ReverseProxy) findTargetForRequest(req *http.Request) (*url.URL, string, types.AccountID, bool) {
p.mappingsMux.RLock()
if p.mappings == nil {
p.mappingsMux.RUnlock()
@@ -27,10 +29,12 @@ func (p *ReverseProxy) findTargetForRequest(req *http.Request) (*url.URL, string
}
defer p.mappingsMux.RUnlock()
host, _, err := net.SplitHostPort(req.Host)
if err != nil {
host = req.Host
// Strip port from host if present (e.g., "external.test:8443" -> "external.test")
host := req.Host
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
m, exists := p.mappings[host]
if !exists {
return nil, "", "", false

View File

@@ -4,139 +4,429 @@ import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"sync"
"time"
"github.com/hashicorp/go-multierror"
log "github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
nberrors "github.com/netbirdio/netbird/client/errors"
"github.com/netbirdio/netbird/client/embed"
"github.com/netbirdio/netbird/proxy/internal/types"
"github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/util"
)
const deviceNamePrefix = "ingress-"
const deviceNamePrefix = "ingress-proxy-"
// ErrNoAccountID is returned when a request context is missing the account ID.
var ErrNoAccountID = errors.New("no account ID in request context")
// domainInfo holds metadata about a registered domain.
type domainInfo struct {
reverseProxyID string
}
// clientEntry holds an embedded NetBird client and tracks which domains use it.
type clientEntry struct {
client *embed.Client
transport *http.Transport
domains map[domain.Domain]domainInfo
createdAt time.Time
started bool
}
type statusNotifier interface {
NotifyStatus(ctx context.Context, accountID, reverseProxyID, domain string, connected bool) error
}
// NetBird provides an http.RoundTripper implementation
// backed by underlying NetBird connections.
// Clients are keyed by AccountID, allowing multiple domains to share the same connection.
type NetBird struct {
mgmtAddr string
proxyID string
logger *log.Logger
clientsMux sync.RWMutex
clients map[string]*embed.Client
clientsMux sync.RWMutex
clients map[types.AccountID]*clientEntry
initLogOnce sync.Once
statusNotifier statusNotifier
}
func NewNetBird(mgmtAddr string, logger *log.Logger) *NetBird {
// NewNetBird creates a new NetBird transport.
func NewNetBird(mgmtAddr, proxyID string, logger *log.Logger, notifier statusNotifier) *NetBird {
if logger == nil {
logger = log.StandardLogger()
}
return &NetBird{
mgmtAddr: mgmtAddr,
logger: logger,
clients: make(map[string]*embed.Client),
mgmtAddr: mgmtAddr,
proxyID: proxyID,
logger: logger,
clients: make(map[types.AccountID]*clientEntry),
statusNotifier: notifier,
}
}
func (n *NetBird) AddPeer(ctx context.Context, domain, key string) error {
// AddPeer registers a domain for an account. If the account doesn't have a client yet,
// one is created using the provided setup key. Multiple domains can share the same client.
func (n *NetBird) AddPeer(ctx context.Context, accountID types.AccountID, d domain.Domain, key, reverseProxyID string) error {
n.clientsMux.Lock()
entry, exists := n.clients[accountID]
if exists {
// Client already exists for this account, just register the domain
entry.domains[d] = domainInfo{reverseProxyID: reverseProxyID}
started := entry.started
n.clientsMux.Unlock()
n.logger.WithFields(log.Fields{
"account_id": accountID,
"domain": d,
}).Debug("registered domain with existing client")
// If client is already started, notify this domain as connected immediately
if started && n.statusNotifier != nil {
if err := n.statusNotifier.NotifyStatus(ctx, string(accountID), reverseProxyID, string(d), true); err != nil {
n.logger.WithFields(log.Fields{
"account_id": accountID,
"domain": d,
}).WithError(err).Warn("failed to notify status for existing client")
}
}
return nil
}
n.initLogOnce.Do(func() {
if err := util.InitLog(log.WarnLevel.String(), util.LogConsole); err != nil {
n.logger.WithField("account_id", accountID).Warnf("failed to initialize embedded client logging: %v", err)
}
})
wgPort := 0
client, err := embed.New(embed.Options{
DeviceName: deviceNamePrefix + domain,
DeviceName: deviceNamePrefix + n.proxyID,
ManagementURL: n.mgmtAddr,
SetupKey: key,
LogOutput: io.Discard,
LogLevel: log.WarnLevel.String(),
BlockInbound: true,
WireguardPort: &wgPort,
})
if err != nil {
n.clientsMux.Unlock()
return fmt.Errorf("create netbird client: %w", err)
}
// Create a transport using the client dialer. We do this instead of using
// the client's HTTPClient to avoid issues with request validation that do
// not work with reverse proxied requests.
entry = &clientEntry{
client: client,
domains: map[domain.Domain]domainInfo{d: {reverseProxyID: reverseProxyID}},
transport: &http.Transport{
DialContext: client.DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
createdAt: time.Now(),
started: false,
}
n.clients[accountID] = entry
n.clientsMux.Unlock()
n.logger.WithFields(log.Fields{
"account_id": accountID,
"domain": d,
}).Info("created new client for account")
// Attempt to start the client in the background, if this fails
// then it is not ideal, but it isn't the end of the world because
// we will try to start the client again before we use it.
go func() {
startCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
startCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err = client.Start(startCtx)
switch {
case errors.Is(err, context.DeadlineExceeded):
n.logger.Debug("netbird client timed out")
// This is not ideal, but we will try again later.
if err := client.Start(startCtx); err != nil {
if errors.Is(err, context.DeadlineExceeded) {
n.logger.WithFields(log.Fields{
"account_id": accountID,
}).Debug("netbird client start timed out, will retry on first request")
} else {
n.logger.WithFields(log.Fields{
"account_id": accountID,
}).WithError(err).Error("failed to start netbird client")
}
return
case err != nil:
n.logger.WithField("domain", domain).WithError(err).Error("Unable to start netbird client, will try again later.")
}
// Mark client as started and notify all registered domains
n.clientsMux.Lock()
entry, exists := n.clients[accountID]
if exists {
entry.started = true
}
// Copy domain info while holding lock
var domainsToNotify []struct {
domain domain.Domain
reverseProxyID string
}
if exists {
for dom, info := range entry.domains {
domainsToNotify = append(domainsToNotify, struct {
domain domain.Domain
reverseProxyID string
}{domain: dom, reverseProxyID: info.reverseProxyID})
}
}
n.clientsMux.Unlock()
// Notify all domains that they're connected
if n.statusNotifier != nil {
for _, domInfo := range domainsToNotify {
if err := n.statusNotifier.NotifyStatus(ctx, string(accountID), domInfo.reverseProxyID, string(domInfo.domain), true); err != nil {
n.logger.WithFields(log.Fields{
"account_id": accountID,
"domain": domInfo.domain,
}).WithError(err).Warn("failed to notify tunnel connection status")
} else {
n.logger.WithFields(log.Fields{
"account_id": accountID,
"domain": domInfo.domain,
}).Info("notified management about tunnel connection")
}
}
}
}()
n.clientsMux.Lock()
defer n.clientsMux.Unlock()
n.clients[domain] = client
return nil
}
func (n *NetBird) RemovePeer(ctx context.Context, domain string) error {
n.clientsMux.RLock()
client, exists := n.clients[domain]
n.clientsMux.RUnlock()
// RemovePeer unregisters a domain from an account. The client is only stopped
// when no domains are using it anymore.
func (n *NetBird) RemovePeer(ctx context.Context, accountID types.AccountID, d domain.Domain) error {
n.clientsMux.Lock()
entry, exists := n.clients[accountID]
if !exists {
// Mission failed successfully!
n.clientsMux.Unlock()
return nil
}
if err := client.Stop(ctx); err != nil {
return fmt.Errorf("stop netbird client: %w", err)
// Get domain info before deleting
domInfo, domainExists := entry.domains[d]
if !domainExists {
n.clientsMux.Unlock()
return nil
}
n.clientsMux.Lock()
defer n.clientsMux.Unlock()
delete(n.clients, domain)
delete(entry.domains, d)
// If there are still domains using this client, keep it running
if len(entry.domains) > 0 {
n.clientsMux.Unlock()
n.logger.WithFields(log.Fields{
"account_id": accountID,
"domain": d,
"remaining_domains": len(entry.domains),
}).Debug("unregistered domain, client still in use")
// Notify this domain as disconnected
if n.statusNotifier != nil {
if err := n.statusNotifier.NotifyStatus(ctx, string(accountID), domInfo.reverseProxyID, string(d), false); err != nil {
n.logger.WithFields(log.Fields{
"account_id": accountID,
"domain": d,
}).WithError(err).Warn("failed to notify tunnel disconnection status")
}
}
return nil
}
// No more domains using this client, stop it
n.logger.WithFields(log.Fields{
"account_id": accountID,
}).Info("stopping client, no more domains")
client := entry.client
transport := entry.transport
delete(n.clients, accountID)
n.clientsMux.Unlock()
// Notify disconnection before stopping
if n.statusNotifier != nil {
if err := n.statusNotifier.NotifyStatus(ctx, string(accountID), domInfo.reverseProxyID, string(d), false); err != nil {
n.logger.WithFields(log.Fields{
"account_id": accountID,
"domain": d,
}).WithError(err).Warn("failed to notify tunnel disconnection status")
}
}
transport.CloseIdleConnections()
if err := client.Stop(ctx); err != nil {
n.logger.WithFields(log.Fields{
"account_id": accountID,
}).WithError(err).Warn("failed to stop netbird client")
}
return nil
}
// RoundTrip implements http.RoundTripper. It looks up the client for the account
// specified in the request context and uses it to dial the backend.
func (n *NetBird) RoundTrip(req *http.Request) (*http.Response, error) {
host, _, err := net.SplitHostPort(req.Host)
if err != nil {
host = req.Host
accountID := AccountIDFromContext(req.Context())
if accountID == "" {
return nil, ErrNoAccountID
}
// Copy references while holding lock, then unlock early to avoid blocking
// other requests during the potentially slow RoundTrip.
n.clientsMux.RLock()
client, exists := n.clients[host]
// Immediately unlock after retrieval here rather than defer to avoid
// the call to client.Do blocking other clients being used whilst one
// is in use.
n.clientsMux.RUnlock()
entry, exists := n.clients[accountID]
if !exists {
return nil, fmt.Errorf("no peer connection found for host: %s", host)
n.clientsMux.RUnlock()
return nil, fmt.Errorf("no peer connection found for account: %s", accountID)
}
client := entry.client
transport := entry.transport
n.clientsMux.RUnlock()
// Attempt to start the client, if the client is already running then
// it will return an error that we ignore, if this hits a timeout then
// this request is unprocessable.
startCtx, cancel := context.WithTimeout(req.Context(), 3*time.Second)
startCtx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
defer cancel()
err = client.Start(startCtx)
switch {
case errors.Is(err, embed.ErrClientAlreadyStarted):
break
case err != nil:
return nil, fmt.Errorf("start netbird client: %w", err)
if err := client.Start(startCtx); err != nil {
if !errors.Is(err, embed.ErrClientAlreadyStarted) {
return nil, fmt.Errorf("start netbird client: %w", err)
}
}
n.logger.WithFields(log.Fields{
"host": host,
"account_id": accountID,
"host": req.Host,
"url": req.URL.String(),
"requestURI": req.RequestURI,
"method": req.Method,
}).Debug("running roundtrip for peer connection")
// Create a new transport using the client dialer and perform the roundtrip.
// We do this instead of using the client HTTPClient to avoid issues around
// client request validation that do not work with the reverse proxied
// requests.
// Other values are simply copied from the http.DefaultTransport which the
// standard reverse proxy implementation would have used.
// TODO: tune this transport for our needs.
return (&http.Transport{
DialContext: client.DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}).RoundTrip(req)
return transport.RoundTrip(req)
}
// StopAll stops all clients.
func (n *NetBird) StopAll(ctx context.Context) error {
n.clientsMux.Lock()
defer n.clientsMux.Unlock()
var merr *multierror.Error
for accountID, entry := range n.clients {
entry.transport.CloseIdleConnections()
if err := entry.client.Stop(ctx); err != nil {
n.logger.WithFields(log.Fields{
"account_id": accountID,
}).WithError(err).Warn("failed to stop netbird client during shutdown")
merr = multierror.Append(merr, err)
}
}
maps.Clear(n.clients)
return nberrors.FormatErrorOrNil(merr)
}
// HasClient returns true if there is a client for the given account.
func (n *NetBird) HasClient(accountID types.AccountID) bool {
n.clientsMux.RLock()
defer n.clientsMux.RUnlock()
_, exists := n.clients[accountID]
return exists
}
// DomainCount returns the number of domains registered for the given account.
// Returns 0 if the account has no client.
func (n *NetBird) DomainCount(accountID types.AccountID) int {
n.clientsMux.RLock()
defer n.clientsMux.RUnlock()
entry, exists := n.clients[accountID]
if !exists {
return 0
}
return len(entry.domains)
}
// ClientCount returns the total number of active clients.
func (n *NetBird) ClientCount() int {
n.clientsMux.RLock()
defer n.clientsMux.RUnlock()
return len(n.clients)
}
// GetClient returns the embed.Client for the given account ID.
func (n *NetBird) GetClient(accountID types.AccountID) (*embed.Client, bool) {
n.clientsMux.RLock()
defer n.clientsMux.RUnlock()
entry, exists := n.clients[accountID]
if !exists {
return nil, false
}
return entry.client, true
}
// ClientDebugInfo contains debug information about a client.
type ClientDebugInfo struct {
AccountID types.AccountID
DomainCount int
Domains domain.List
HasClient bool
CreatedAt time.Time
}
// ListClientsForDebug returns information about all clients for debug purposes.
func (n *NetBird) ListClientsForDebug() map[types.AccountID]ClientDebugInfo {
n.clientsMux.RLock()
defer n.clientsMux.RUnlock()
result := make(map[types.AccountID]ClientDebugInfo)
for accountID, entry := range n.clients {
domains := make(domain.List, 0, len(entry.domains))
for d := range entry.domains {
domains = append(domains, d)
}
result[accountID] = ClientDebugInfo{
AccountID: accountID,
DomainCount: len(entry.domains),
Domains: domains,
HasClient: entry.client != nil,
CreatedAt: entry.createdAt,
}
}
return result
}
// accountIDContextKey is the context key for storing the account ID.
type accountIDContextKey struct{}
// WithAccountID adds the account ID to the context.
func WithAccountID(ctx context.Context, accountID types.AccountID) context.Context {
return context.WithValue(ctx, accountIDContextKey{}, accountID)
}
// AccountIDFromContext retrieves the account ID from the context.
func AccountIDFromContext(ctx context.Context) types.AccountID {
v := ctx.Value(accountIDContextKey{})
if v == nil {
return ""
}
accountID, ok := v.(types.AccountID)
if !ok {
return ""
}
return accountID
}

View File

@@ -0,0 +1,247 @@
package roundtrip
import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/netbirdio/netbird/proxy/internal/types"
"github.com/netbirdio/netbird/shared/management/domain"
)
// mockNetBird creates a NetBird instance for testing without actually connecting.
// It uses an invalid management URL to prevent real connections.
func mockNetBird() *NetBird {
return NewNetBird("http://invalid.test:9999", "test-proxy", nil, nil)
}
func TestNetBird_AddPeer_CreatesClientForNewAccount(t *testing.T) {
nb := mockNetBird()
accountID := types.AccountID("account-1")
// Initially no client exists.
assert.False(t, nb.HasClient(accountID), "should not have client before AddPeer")
assert.Equal(t, 0, nb.DomainCount(accountID), "domain count should be 0")
// Add first domain - this should create a new client.
// Note: This will fail to actually connect since we use an invalid URL,
// but the client entry should still be created.
err := nb.AddPeer(context.Background(), accountID, domain.Domain("domain1.test"), "setup-key-1", "proxy-1")
require.NoError(t, err)
assert.True(t, nb.HasClient(accountID), "should have client after AddPeer")
assert.Equal(t, 1, nb.DomainCount(accountID), "domain count should be 1")
}
func TestNetBird_AddPeer_ReuseClientForSameAccount(t *testing.T) {
nb := mockNetBird()
accountID := types.AccountID("account-1")
// Add first domain.
err := nb.AddPeer(context.Background(), accountID, domain.Domain("domain1.test"), "setup-key-1", "proxy-1")
require.NoError(t, err)
assert.Equal(t, 1, nb.DomainCount(accountID))
// Add second domain for the same account - should reuse existing client.
err = nb.AddPeer(context.Background(), accountID, domain.Domain("domain2.test"), "setup-key-1", "proxy-2")
require.NoError(t, err)
assert.Equal(t, 2, nb.DomainCount(accountID), "domain count should be 2 after adding second domain")
// Add third domain.
err = nb.AddPeer(context.Background(), accountID, domain.Domain("domain3.test"), "setup-key-1", "proxy-3")
require.NoError(t, err)
assert.Equal(t, 3, nb.DomainCount(accountID), "domain count should be 3 after adding third domain")
// Still only one client.
assert.True(t, nb.HasClient(accountID))
}
func TestNetBird_AddPeer_SeparateClientsForDifferentAccounts(t *testing.T) {
nb := mockNetBird()
account1 := types.AccountID("account-1")
account2 := types.AccountID("account-2")
// Add domain for account 1.
err := nb.AddPeer(context.Background(), account1, domain.Domain("domain1.test"), "setup-key-1", "proxy-1")
require.NoError(t, err)
// Add domain for account 2.
err = nb.AddPeer(context.Background(), account2, domain.Domain("domain2.test"), "setup-key-2", "proxy-2")
require.NoError(t, err)
// Both accounts should have their own clients.
assert.True(t, nb.HasClient(account1), "account1 should have client")
assert.True(t, nb.HasClient(account2), "account2 should have client")
assert.Equal(t, 1, nb.DomainCount(account1), "account1 domain count should be 1")
assert.Equal(t, 1, nb.DomainCount(account2), "account2 domain count should be 1")
}
func TestNetBird_RemovePeer_KeepsClientWhenDomainsRemain(t *testing.T) {
nb := mockNetBird()
accountID := types.AccountID("account-1")
// Add multiple domains.
err := nb.AddPeer(context.Background(), accountID, domain.Domain("domain1.test"), "setup-key-1", "proxy-1")
require.NoError(t, err)
err = nb.AddPeer(context.Background(), accountID, domain.Domain("domain2.test"), "setup-key-1", "proxy-2")
require.NoError(t, err)
err = nb.AddPeer(context.Background(), accountID, domain.Domain("domain3.test"), "setup-key-1", "proxy-3")
require.NoError(t, err)
assert.Equal(t, 3, nb.DomainCount(accountID))
// Remove one domain - client should remain.
err = nb.RemovePeer(context.Background(), accountID, "domain1.test")
require.NoError(t, err)
assert.True(t, nb.HasClient(accountID), "client should remain after removing one domain")
assert.Equal(t, 2, nb.DomainCount(accountID), "domain count should be 2")
// Remove another domain - client should still remain.
err = nb.RemovePeer(context.Background(), accountID, "domain2.test")
require.NoError(t, err)
assert.True(t, nb.HasClient(accountID), "client should remain after removing second domain")
assert.Equal(t, 1, nb.DomainCount(accountID), "domain count should be 1")
}
func TestNetBird_RemovePeer_RemovesClientWhenLastDomainRemoved(t *testing.T) {
nb := mockNetBird()
accountID := types.AccountID("account-1")
// Add single domain.
err := nb.AddPeer(context.Background(), accountID, domain.Domain("domain1.test"), "setup-key-1", "proxy-1")
require.NoError(t, err)
assert.True(t, nb.HasClient(accountID))
// Remove the only domain - client should be removed.
// Note: Stop() may fail since the client never actually connected,
// but the entry should still be removed from the map.
_ = nb.RemovePeer(context.Background(), accountID, "domain1.test")
// After removing all domains, client should be gone.
assert.False(t, nb.HasClient(accountID), "client should be removed after removing last domain")
assert.Equal(t, 0, nb.DomainCount(accountID), "domain count should be 0")
}
func TestNetBird_RemovePeer_NonExistentAccountIsNoop(t *testing.T) {
nb := mockNetBird()
accountID := types.AccountID("nonexistent-account")
// Removing from non-existent account should not error.
err := nb.RemovePeer(context.Background(), accountID, "domain1.test")
assert.NoError(t, err, "removing from non-existent account should not error")
}
func TestNetBird_RemovePeer_NonExistentDomainIsNoop(t *testing.T) {
nb := mockNetBird()
accountID := types.AccountID("account-1")
// Add one domain.
err := nb.AddPeer(context.Background(), accountID, domain.Domain("domain1.test"), "setup-key-1", "proxy-1")
require.NoError(t, err)
// Remove non-existent domain - should not affect existing domain.
err = nb.RemovePeer(context.Background(), accountID, domain.Domain("nonexistent.test"))
require.NoError(t, err)
// Original domain should still be registered.
assert.True(t, nb.HasClient(accountID))
assert.Equal(t, 1, nb.DomainCount(accountID), "original domain should remain")
}
func TestWithAccountID_AndAccountIDFromContext(t *testing.T) {
ctx := context.Background()
accountID := types.AccountID("test-account")
// Initially no account ID in context.
retrieved := AccountIDFromContext(ctx)
assert.True(t, retrieved == "", "should be empty when not set")
// Add account ID to context.
ctx = WithAccountID(ctx, accountID)
retrieved = AccountIDFromContext(ctx)
assert.Equal(t, accountID, retrieved, "should retrieve the same account ID")
}
func TestAccountIDFromContext_ReturnsEmptyForWrongType(t *testing.T) {
// Create context with wrong type for account ID key.
ctx := context.WithValue(context.Background(), accountIDContextKey{}, "wrong-type-string")
retrieved := AccountIDFromContext(ctx)
assert.True(t, retrieved == "", "should return empty for wrong type")
}
func TestNetBird_StopAll_StopsAllClients(t *testing.T) {
nb := mockNetBird()
account1 := types.AccountID("account-1")
account2 := types.AccountID("account-2")
account3 := types.AccountID("account-3")
// Add domains for multiple accounts.
err := nb.AddPeer(context.Background(), account1, domain.Domain("domain1.test"), "key-1", "proxy-1")
require.NoError(t, err)
err = nb.AddPeer(context.Background(), account2, domain.Domain("domain2.test"), "key-2", "proxy-2")
require.NoError(t, err)
err = nb.AddPeer(context.Background(), account3, domain.Domain("domain3.test"), "key-3", "proxy-3")
require.NoError(t, err)
assert.Equal(t, 3, nb.ClientCount(), "should have 3 clients")
// Stop all clients.
// Note: StopAll may return errors since clients never actually connected,
// but the clients should still be removed from the map.
_ = nb.StopAll(context.Background())
assert.Equal(t, 0, nb.ClientCount(), "should have 0 clients after StopAll")
assert.False(t, nb.HasClient(account1), "account1 should not have client")
assert.False(t, nb.HasClient(account2), "account2 should not have client")
assert.False(t, nb.HasClient(account3), "account3 should not have client")
}
func TestNetBird_ClientCount(t *testing.T) {
nb := mockNetBird()
assert.Equal(t, 0, nb.ClientCount(), "should start with 0 clients")
// Add clients for different accounts.
err := nb.AddPeer(context.Background(), types.AccountID("account-1"), domain.Domain("domain1.test"), "key-1", "proxy-1")
require.NoError(t, err)
assert.Equal(t, 1, nb.ClientCount())
err = nb.AddPeer(context.Background(), types.AccountID("account-2"), domain.Domain("domain2.test"), "key-2", "proxy-2")
require.NoError(t, err)
assert.Equal(t, 2, nb.ClientCount())
// Adding domain to existing account should not increase count.
err = nb.AddPeer(context.Background(), types.AccountID("account-1"), domain.Domain("domain1b.test"), "key-1", "proxy-1b")
require.NoError(t, err)
assert.Equal(t, 2, nb.ClientCount(), "adding domain to existing account should not increase client count")
}
func TestNetBird_RoundTrip_RequiresAccountIDInContext(t *testing.T) {
nb := mockNetBird()
// Create a request without account ID in context.
req, err := http.NewRequest("GET", "http://example.com/", nil)
require.NoError(t, err)
// RoundTrip should fail because no account ID in context.
_, err = nb.RoundTrip(req)
require.ErrorIs(t, err, ErrNoAccountID)
}
func TestNetBird_RoundTrip_RequiresExistingClient(t *testing.T) {
nb := mockNetBird()
accountID := types.AccountID("nonexistent-account")
// Create a request with account ID but no client exists.
req, err := http.NewRequest("GET", "http://example.com/", nil)
require.NoError(t, err)
req = req.WithContext(WithAccountID(req.Context(), accountID))
// RoundTrip should fail because no client for this account.
_, err = nb.RoundTrip(req)
assert.Error(t, err)
assert.Contains(t, err.Error(), "no peer connection found for account")
}

View File

@@ -0,0 +1,5 @@
// Package types defines common types used across the proxy package.
package types
// AccountID represents a unique identifier for a NetBird account.
type AccountID string

View File

@@ -31,20 +31,24 @@ import (
"github.com/netbirdio/netbird/proxy/internal/accesslog"
"github.com/netbirdio/netbird/proxy/internal/acme"
"github.com/netbirdio/netbird/proxy/internal/auth"
"github.com/netbirdio/netbird/proxy/internal/debug"
"github.com/netbirdio/netbird/proxy/internal/proxy"
"github.com/netbirdio/netbird/proxy/internal/roundtrip"
"github.com/netbirdio/netbird/proxy/internal/types"
"github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/shared/management/proto"
"github.com/netbirdio/netbird/util/embeddedroots"
)
type Server struct {
mgmtConn *grpc.ClientConn
proxy *proxy.ReverseProxy
netbird *roundtrip.NetBird
acme *acme.Manager
auth *auth.Middleware
http *http.Server
https *http.Server
mgmtClient proto.ProxyServiceClient
proxy *proxy.ReverseProxy
netbird *roundtrip.NetBird
acme *acme.Manager
auth *auth.Middleware
http *http.Server
https *http.Server
debug *http.Server
// Mostly used for debugging on management.
startTime time.Time
@@ -62,6 +66,38 @@ type Server struct {
OIDCClientSecret string
OIDCEndpoint string
OIDCScopes []string
// DebugEndpointEnabled enables the debug HTTP endpoint.
DebugEndpointEnabled bool
// DebugEndpointAddress is the address for the debug HTTP endpoint (default: ":8444").
DebugEndpointAddress string
}
// NotifyStatus sends a status update to management about tunnel connectivity
func (s *Server) NotifyStatus(ctx context.Context, accountID, reverseProxyID, domain string, connected bool) error {
status := proto.ProxyStatus_PROXY_STATUS_TUNNEL_NOT_CREATED
if connected {
status = proto.ProxyStatus_PROXY_STATUS_ACTIVE
}
_, err := s.mgmtClient.SendStatusUpdate(ctx, &proto.SendStatusUpdateRequest{
ReverseProxyId: reverseProxyID,
AccountId: accountID,
Status: status,
CertificateIssued: false,
})
return err
}
// NotifyCertificateIssued sends a notification to management that a certificate was issued
func (s *Server) NotifyCertificateIssued(ctx context.Context, accountID, reverseProxyID, domain string) error {
_, err := s.mgmtClient.SendStatusUpdate(ctx, &proto.SendStatusUpdateRequest{
ReverseProxyId: reverseProxyID,
AccountId: accountID,
Status: proto.ProxyStatus_PROXY_STATUS_ACTIVE,
CertificateIssued: true,
})
return err
}
func (s *Server) ListenAndServe(ctx context.Context, addr string) (err error) {
@@ -105,7 +141,7 @@ func (s *Server) ListenAndServe(ctx context.Context, addr string) (err error) {
"gRPC_address": mgmtURL.Host,
"TLS_enabled": mgmtURL.Scheme == "https",
}).Debug("starting management gRPC client")
s.mgmtConn, err = grpc.NewClient(mgmtURL.Host,
mgmtConn, err := grpc.NewClient(mgmtURL.Host,
grpc.WithTransportCredentials(creds),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 20 * time.Second,
@@ -116,18 +152,18 @@ func (s *Server) ListenAndServe(ctx context.Context, addr string) (err error) {
if err != nil {
return fmt.Errorf("could not create management connection: %w", err)
}
mgmtClient := proto.NewProxyServiceClient(s.mgmtConn)
go s.newManagementMappingWorker(ctx, mgmtClient)
s.mgmtClient = proto.NewProxyServiceClient(mgmtConn)
go s.newManagementMappingWorker(ctx, s.mgmtClient)
// Initialize the netbird client, this is required to build peer connections
// to proxy over.
s.netbird = roundtrip.NewNetBird(s.ManagementAddress, s.Logger)
s.netbird = roundtrip.NewNetBird(s.ManagementAddress, s.ID, s.Logger, s)
// When generating ACME certificates, start a challenge server.
tlsConfig := &tls.Config{}
if s.GenerateACMECertificates {
s.Logger.WithField("acme_server", s.ACMEDirectory).Debug("ACME certificates enabled, configuring certificate manager")
s.acme = acme.NewManager(s.CertificateDirectory, s.ACMEDirectory)
s.acme = acme.NewManager(s.CertificateDirectory, s.ACMEDirectory, s)
s.http = &http.Server{
Addr: s.ACMEChallengeAddress,
Handler: s.acme.HTTPHandler(nil),
@@ -175,7 +211,35 @@ func (s *Server) ListenAndServe(ctx context.Context, addr string) (err error) {
s.auth = auth.NewMiddleware()
// Configure Access logs to management server.
accessLog := accesslog.NewLogger(mgmtClient, s.Logger)
accessLog := accesslog.NewLogger(s.mgmtClient, s.Logger)
if s.DebugEndpointEnabled {
debugAddr := debugEndpointAddr(s.DebugEndpointAddress)
debugHandler := debug.NewHandler(s.netbird, s.Logger)
s.debug = &http.Server{
Addr: debugAddr,
Handler: debugHandler,
}
go func() {
s.Logger.WithField("address", debugAddr).Info("starting debug endpoint")
if err := s.debug.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
s.Logger.Errorf("debug endpoint error: %v", err)
}
}()
defer func() {
if err := s.debug.Close(); err != nil {
s.Logger.Debugf("debug endpoint close: %v", err)
}
}()
}
defer func() {
stopCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := s.netbird.StopAll(stopCtx); err != nil {
s.Logger.Warnf("failed to stop all netbird clients: %v", err)
}
}()
// Finally, start the reverse proxy.
s.https = &http.Server{
@@ -279,11 +343,15 @@ func (s *Server) handleMappingStream(ctx context.Context, mappingClient proto.Pr
}
func (s *Server) addMapping(ctx context.Context, mapping *proto.ProxyMapping) error {
if err := s.netbird.AddPeer(ctx, mapping.GetDomain(), mapping.GetSetupKey()); err != nil {
return fmt.Errorf("create peer for domain %q: %w", mapping.GetDomain(), err)
d := domain.Domain(mapping.GetDomain())
accountID := types.AccountID(mapping.GetAccountId())
reverseProxyID := mapping.GetId()
if err := s.netbird.AddPeer(ctx, accountID, d, mapping.GetSetupKey(), reverseProxyID); err != nil {
return fmt.Errorf("create peer for domain %q: %w", d, err)
}
if s.acme != nil {
s.acme.AddDomain(mapping.GetDomain())
s.acme.AddDomain(string(d), string(accountID), reverseProxyID)
}
// Pass the mapping through to the update function to avoid duplicating the
@@ -299,13 +367,12 @@ func (s *Server) updateMapping(ctx context.Context, mapping *proto.ProxyMapping)
// the auth and proxy mappings.
// Note: this does require the management server to always send a
// full mapping rather than deltas during a modification.
mgmtClient := proto.NewProxyServiceClient(s.mgmtConn)
var schemes []auth.Scheme
if mapping.GetAuth().GetPassword() {
schemes = append(schemes, auth.NewPassword(mgmtClient, mapping.GetId(), mapping.GetAccountId()))
schemes = append(schemes, auth.NewPassword(s.mgmtClient, mapping.GetId(), mapping.GetAccountId()))
}
if mapping.GetAuth().GetPin() {
schemes = append(schemes, auth.NewPin(mgmtClient, mapping.GetId(), mapping.GetAccountId()))
schemes = append(schemes, auth.NewPin(s.mgmtClient, mapping.GetId(), mapping.GetAccountId()))
}
if mapping.GetAuth().GetOidc() != nil {
oidc := mapping.GetAuth().GetOidc()
@@ -317,17 +384,20 @@ func (s *Server) updateMapping(ctx context.Context, mapping *proto.ProxyMapping)
}))
}
if mapping.GetAuth().GetLink() {
schemes = append(schemes, auth.NewLink(mgmtClient, mapping.GetId(), mapping.GetAccountId()))
schemes = append(schemes, auth.NewLink(s.mgmtClient, mapping.GetId(), mapping.GetAccountId()))
}
s.auth.AddDomain(mapping.GetDomain(), schemes)
s.proxy.AddMapping(s.protoToMapping(mapping))
}
func (s *Server) removeMapping(ctx context.Context, mapping *proto.ProxyMapping) {
if err := s.netbird.RemovePeer(ctx, mapping.GetDomain()); err != nil {
d := domain.Domain(mapping.GetDomain())
accountID := types.AccountID(mapping.GetAccountId())
if err := s.netbird.RemovePeer(ctx, accountID, d); err != nil {
s.Logger.WithFields(log.Fields{
"domain": mapping.GetDomain(),
"error": err,
"account_id": accountID,
"domain": d,
"error": err,
}).Error("Error removing NetBird peer connection for domain, continuing additional domain cleanup but peer connection may still exist")
}
if s.acme != nil {
@@ -356,8 +426,17 @@ func (s *Server) protoToMapping(mapping *proto.ProxyMapping) proxy.Mapping {
}
return proxy.Mapping{
ID: mapping.GetId(),
AccountID: mapping.AccountId,
AccountID: types.AccountID(mapping.GetAccountId()),
Host: mapping.GetDomain(),
Paths: paths,
}
}
// debugEndpointAddr returns the address for the debug endpoint.
// If addr is empty, it defaults to localhost:8444 for security.
func debugEndpointAddr(addr string) string {
if addr == "" {
return "localhost:8444"
}
return addr
}

48
proxy/server_test.go Normal file
View File

@@ -0,0 +1,48 @@
package proxy
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDebugEndpointDisabledByDefault(t *testing.T) {
s := &Server{}
assert.False(t, s.DebugEndpointEnabled, "debug endpoint should be disabled by default")
}
func TestDebugEndpointAddr(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "empty defaults to localhost",
input: "",
expected: "localhost:8444",
},
{
name: "explicit localhost preserved",
input: "localhost:9999",
expected: "localhost:9999",
},
{
name: "explicit address preserved",
input: "0.0.0.0:8444",
expected: "0.0.0.0:8444",
},
{
name: "127.0.0.1 preserved",
input: "127.0.0.1:8444",
expected: "127.0.0.1:8444",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := debugEndpointAddr(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}

23
proxy/web/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,19 @@
<svg width="133" height="23" viewBox="0 0 133 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_0_3)">
<path d="M46.9438 7.5013C48.1229 8.64688 48.7082 10.3025 48.7082 12.4683V21.6663H46.1411V12.8362C46.1411 11.2809 45.7481 10.0851 44.9704 9.26566C44.1928 8.43783 43.1308 8.0281 41.7846 8.0281C40.4383 8.0281 39.3345 8.45455 38.5234 9.30747C37.7123 10.1604 37.3109 11.4063 37.3109 13.0369V21.6663H34.7188V6.06305H37.3109V8.28732C37.821 7.49294 38.5234 6.87416 39.4014 6.43934C40.2878 6.00452 41.2578 5.78711 42.3197 5.78711C44.2179 5.78711 45.7565 6.36408 46.9355 7.50966L46.9438 7.5013Z" fill="#F2F2F2"/>
<path d="M67.1048 14.8344H54.6288C54.7208 16.373 55.2476 17.5771 56.2092 18.4384C57.1708 19.2997 58.3331 19.7345 59.6961 19.7345C60.8166 19.7345 61.7531 19.4753 62.4973 18.9485C63.2499 18.4301 63.7767 17.7277 64.0777 16.858H66.8706C66.4525 18.3548 65.6163 19.5756 64.3621 20.5205C63.1078 21.4571 61.5525 21.9337 59.6878 21.9337C58.2077 21.9337 56.8865 21.5992 55.7159 20.9386C54.5452 20.278 53.6337 19.3331 52.9648 18.1039C52.2958 16.8831 51.9697 15.4616 51.9697 13.8477C51.9697 12.2339 52.2958 10.8207 52.9397 9.60825C53.5836 8.39578 54.495 7.45924 55.6573 6.80702C56.828 6.15479 58.1659 5.82031 59.6878 5.82031C61.2096 5.82031 62.4806 6.14643 63.6178 6.79029C64.7551 7.43416 65.6331 8.32052 66.2518 9.44938C66.8706 10.5782 67.18 11.8576 67.18 13.2791C67.18 13.7725 67.1549 14.2909 67.0964 14.8428L67.1048 14.8344ZM63.8603 10.1769C63.4255 9.4661 62.8318 8.92258 62.0793 8.55465C61.3267 8.18673 60.4989 8.00277 59.5874 8.00277C58.2746 8.00277 57.1625 8.42086 56.2427 9.25705C55.3228 10.0932 54.796 11.2472 54.6623 12.7356H64.5126C64.5126 11.7489 64.2952 10.896 63.8603 10.1852V10.1769Z" fill="#F2F2F2"/>
<path d="M73.7695 8.20355V17.4016C73.7695 18.1626 73.9284 18.6977 74.2545 19.0071C74.5806 19.3165 75.1409 19.4754 75.9352 19.4754H77.8418V21.6662H75.5088C74.0622 21.6662 72.9835 21.3317 72.2644 20.6711C71.5452 20.0105 71.1857 18.9151 71.1857 17.3933V8.19519H69.1621V6.0629H71.1857V2.13281H73.7779V6.0629H77.8501V8.19519H73.7779L73.7695 8.20355Z" fill="#F2F2F2"/>
<path d="M85.9022 6.68902C86.9307 6.10369 88.093 5.80266 89.4058 5.80266C90.8106 5.80266 92.0732 6.13714 93.1937 6.79773C94.3142 7.46668 95.2006 8.39485 95.8444 9.59896C96.4883 10.8031 96.8144 12.2079 96.8144 13.7966C96.8144 15.3854 96.4883 16.7818 95.8444 18.011C95.2006 19.2486 94.3142 20.2018 93.1854 20.8875C92.0565 21.5732 90.7939 21.916 89.4141 21.916C88.0344 21.916 86.8805 21.6234 85.8687 21.0297C84.8569 20.4443 84.0876 19.6918 83.5775 18.7803V21.6568H80.9854V0.601562H83.5775V8.97182C84.1127 8.04365 84.8904 7.28272 85.9105 6.69738L85.9022 6.68902ZM93.4529 10.7362C92.9763 9.86654 92.3408 9.19759 91.5297 8.74605C90.7186 8.29451 89.8322 8.06037 88.8706 8.06037C87.909 8.06037 87.0394 8.29451 86.2366 8.75441C85.4255 9.22268 84.7817 9.89163 84.2967 10.778C83.8117 11.6643 83.5692 12.6845 83.5692 13.8384C83.5692 14.9924 83.8117 16.046 84.2967 16.9323C84.7817 17.8187 85.4255 18.4877 86.2366 18.9559C87.0394 19.4242 87.9174 19.65 88.8706 19.65C89.8239 19.65 90.727 19.4158 91.5297 18.9559C92.3324 18.4877 92.9763 17.8187 93.4529 16.9323C93.9296 16.046 94.1637 15.0091 94.1637 13.8134C94.1637 12.6176 93.9296 11.6142 93.4529 10.7362Z" fill="#F2F2F2"/>
<path d="M100.318 3.01864C99.9749 2.67581 99.8076 2.25771 99.8076 1.76436C99.8076 1.27101 99.9749 0.852913 100.318 0.510076C100.661 0.167238 101.079 0 101.572 0C102.065 0 102.45 0.167238 102.784 0.510076C103.119 0.852913 103.286 1.27101 103.286 1.76436C103.286 2.25771 103.119 2.67581 102.784 3.01864C102.45 3.36148 102.049 3.52872 101.572 3.52872C101.095 3.52872 100.661 3.36148 100.318 3.01864ZM102.826 6.06237V21.6657H100.234V6.06237H102.826Z" fill="#F2F2F2"/>
<path d="M111.773 6.52155C112.617 6.0282 113.646 5.77734 114.867 5.77734V8.45315H114.181C111.28 8.45315 109.825 10.0252 109.825 13.1776V21.6649H107.232V6.06165H109.825V8.5953C110.276 7.70058 110.928 7.00654 111.773 6.51319V6.52155Z" fill="#F2F2F2"/>
<path d="M117.861 9.60732C118.505 8.40321 119.391 7.46668 120.52 6.80609C121.649 6.1455 122.92 5.81102 124.325 5.81102C125.537 5.81102 126.666 6.09533 127.711 6.64721C128.757 7.20746 129.551 7.94331 130.103 8.85475V0.601562H132.72V21.6735H130.103V18.7385C129.593 19.6667 128.832 20.436 127.828 21.0297C126.825 21.6317 125.646 21.9244 124.3 21.9244C122.953 21.9244 121.657 21.5816 120.528 20.8959C119.4 20.2102 118.513 19.257 117.869 18.0194C117.226 16.7818 116.899 15.377 116.899 13.805C116.899 12.233 117.226 10.8114 117.869 9.60732H117.861ZM129.392 10.7613C128.915 9.89163 128.28 9.22268 127.469 8.75441C126.658 8.28614 125.771 8.06037 124.81 8.06037C123.848 8.06037 122.962 8.28614 122.159 8.74605C121.356 9.20595 120.729 9.86654 120.253 10.7362C119.776 11.6058 119.542 12.6343 119.542 13.8134C119.542 14.9924 119.776 16.046 120.253 16.9323C120.729 17.8187 121.365 18.4877 122.159 18.9559C122.953 19.4242 123.84 19.65 124.81 19.65C125.78 19.65 126.666 19.4158 127.469 18.9559C128.272 18.4877 128.915 17.8187 129.392 16.9323C129.869 16.046 130.103 15.0175 130.103 13.8384C130.103 12.6594 129.869 11.6393 129.392 10.7613Z" fill="#F2F2F2"/>
<path d="M21.4651 0.568359C17.8193 0.902835 16.0047 3.00167 15.3191 4.06363L4.66602 22.5183H17.5182L30.1949 0.568359H21.4651Z" fill="#F68330"/>
<path d="M17.5265 22.5187L0 3.9302C0 3.9302 19.8177 -1.39633 21.7493 15.2188L17.5265 22.5187Z" fill="#F68330"/>
<path d="M14.9255 4.75055L9.54883 14.0657L17.5177 22.5196L21.7405 15.2029C21.0715 9.49174 18.287 6.37276 14.9255 4.74219" fill="#F35E32"/>
</g>
<defs>
<clipPath id="clip0_0_3">
<rect width="132.72" height="22.5186" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

File diff suppressed because one or more lines are too long

19
proxy/web/dist/index.html vendored Normal file
View File

@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/assets/favicon-Cv-2QvSV.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Authentication Required</title>
<meta name="robots" content="noindex, nofollow" />
<script type="module" crossorigin src="/assets/index-BQ7jeUNq.js"></script>
<link rel="stylesheet" crossorigin href="/assets/style-B08XFatU.css">
</head>
<body>
<!-- Go template variables injected here -->
<script>
window.__DATA__ = {{ .Data }};
</script>
<div id="root"></div>
</body>
</html>

2
proxy/web/dist/robots.txt vendored Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /

View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

18
proxy/web/index.html Normal file
View File

@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/src/assets/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Authentication Required</title>
<meta name="robots" content="noindex, nofollow" />
</head>
<body>
<!-- Go template variables injected here -->
<script>
window.__DATA__ = {{ .Data }};
</script>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3939
proxy/web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
proxy/web/package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"clsx": "^2.1.1",
"lucide-react": "^0.468.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^2.6.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@tailwindcss/vite": "^4.1.18",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"tailwindcss": "^4.1.18",
"tsx": "^4.21.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4"
}
}

206
proxy/web/src/App.tsx Normal file
View File

@@ -0,0 +1,206 @@
import { useState, useRef } from "react";
import {Loader2, Lock, Binary, LogIn} from "lucide-react";
import { getData, type Data } from "@/data";
import Button from "@/components/Button";
import { Input } from "@/components/Input";
import PinCodeInput, { type PinCodeInputRef } from "@/components/PinCodeInput";
import { SegmentedTabs } from "@/components/SegmentedTabs";
import { PoweredByNetBird } from "@/components/PoweredByNetBird";
import { Card } from "@/components/Card";
import { Title } from "@/components/Title";
import { Description } from "@/components/Description";
import { Separator } from "@/components/Separator";
import { ErrorMessage } from "@/components/ErrorMessage";
import { Label } from "@/components/Label";
const data = getData();
// For testing, show all methods if none are configured
const methods: NonNullable<Data["methods"]> =
data.methods && Object.keys(data.methods).length > 0
? data.methods
: { password:"password", pin: "pin", oidc: "/auth/oidc" };
function App() {
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState<string | null>(null);
const [pin, setPin] = useState("");
const [password, setPassword] = useState("");
const passwordRef = useRef<HTMLInputElement>(null);
const pinRef = useRef<PinCodeInputRef>(null);
const [activeTab, setActiveTab] = useState<"password" | "pin">(
methods.password ? "password" : "pin"
);
const handleAuthError = (method: "password" | "pin", message: string) => {
setError(message);
setSubmitting(null);
if (method === "password") {
setPassword("");
setTimeout(() => passwordRef.current?.focus(), 200);
} else {
setPin("");
setTimeout(() => pinRef.current?.focus(), 200);
}
};
const submitCredentials = (method: "password" | "pin", value: string) => {
setError(null);
setSubmitting(method);
const formData = new FormData();
if (method === "password") {
formData.append(methods.password!, value);
} else {
formData.append(methods.pin!, value);
}
fetch(window.location.href, {
method: "POST",
body: formData,
redirect: "follow",
})
.then((res) => {
if (res.ok || res.redirected) {
window.location.reload();
} else {
handleAuthError(method, "Authentication failed. Please try again.");
}
})
.catch(() => {
handleAuthError(method, "An error occurred. Please try again.");
});
};
const handlePinChange = (value: string) => {
setPin(value);
if (value.length === 6) {
submitCredentials("pin", value);
}
};
const isPinComplete = pin.length === 6;
const isPasswordEntered = password.length > 0;
const isButtonDisabled = submitting !== null ||
(activeTab === "password" && !isPasswordEntered) ||
(activeTab === "pin" && !isPinComplete);
const hasCredentialAuth = methods.password || methods.pin;
const hasBothCredentials = methods.password && methods.pin;
return (
<main className="mt-20">
<Card className="max-w-105 mx-auto">
<Title>Authentication Required</Title>
<Description>
The service you are trying to access is protected. Please authenticate to continue.
</Description>
<div className="flex flex-col gap-4 mt-7 z-10 relative">
{error && <ErrorMessage error={error} />}
{/* SSO Button */}
{methods.oidc && (
<Button
variant="primary"
className="w-full"
onClick={() => (window.location.href = methods.oidc!)}
>
<LogIn size={16} />
Sign in with SSO
</Button>
)}
{/* Separator */}
{methods.oidc && hasCredentialAuth && <Separator />}
{/* Credential Authentication */}
{hasCredentialAuth && (
<form onSubmit={(e) => {
e.preventDefault();
submitCredentials(activeTab, activeTab === "password" ? password : pin);
}}>
{hasBothCredentials && (
<SegmentedTabs
value={activeTab}
onChange={(v) => {
setActiveTab(v as "password" | "pin");
setTimeout(() => {
if (v === "password") {
passwordRef.current?.focus();
} else {
pinRef.current?.focus();
}
}, 0);
}}
>
<SegmentedTabs.List className="rounded-lg border mb-4">
<SegmentedTabs.Trigger value="password">
<Lock size={14} />
Password
</SegmentedTabs.Trigger>
<SegmentedTabs.Trigger value="pin">
<Binary size={14} />
PIN
</SegmentedTabs.Trigger>
</SegmentedTabs.List>
</SegmentedTabs>
)}
<div className="mb-4">
{methods.password && (activeTab === "password" || !methods.pin) && (
<>
{!hasBothCredentials && <Label>Password</Label>}
<Input
ref={passwordRef}
type="password"
id="password"
placeholder="Enter password"
disabled={submitting !== null}
showPasswordToggle
autoFocus
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</>
)}
{methods.pin && (activeTab === "pin" || !methods.password) && (
<>
{!hasBothCredentials && <Label>Enter PIN Code</Label>}
<PinCodeInput
ref={pinRef}
value={pin}
onChange={handlePinChange}
disabled={submitting !== null}
autoFocus={!methods.password}
/>
</>
)}
</div>
<Button
type="submit"
disabled={isButtonDisabled}
variant="secondary"
className="w-full"
>
{submitting !== null ? (
<>
<Loader2 className="animate-spin" size={16} />
Verifying...
</>
) : (
activeTab === "password" ? "Sign in" : "Submit"
)}
</Button>
</form>
)}
</div>
</Card>
<PoweredByNetBird />
</main>
);
}
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,19 @@
<svg width="133" height="23" viewBox="0 0 133 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_0_3)">
<path d="M46.9438 7.5013C48.1229 8.64688 48.7082 10.3025 48.7082 12.4683V21.6663H46.1411V12.8362C46.1411 11.2809 45.7481 10.0851 44.9704 9.26566C44.1928 8.43783 43.1308 8.0281 41.7846 8.0281C40.4383 8.0281 39.3345 8.45455 38.5234 9.30747C37.7123 10.1604 37.3109 11.4063 37.3109 13.0369V21.6663H34.7188V6.06305H37.3109V8.28732C37.821 7.49294 38.5234 6.87416 39.4014 6.43934C40.2878 6.00452 41.2578 5.78711 42.3197 5.78711C44.2179 5.78711 45.7565 6.36408 46.9355 7.50966L46.9438 7.5013Z" fill="#F2F2F2"/>
<path d="M67.1048 14.8344H54.6288C54.7208 16.373 55.2476 17.5771 56.2092 18.4384C57.1708 19.2997 58.3331 19.7345 59.6961 19.7345C60.8166 19.7345 61.7531 19.4753 62.4973 18.9485C63.2499 18.4301 63.7767 17.7277 64.0777 16.858H66.8706C66.4525 18.3548 65.6163 19.5756 64.3621 20.5205C63.1078 21.4571 61.5525 21.9337 59.6878 21.9337C58.2077 21.9337 56.8865 21.5992 55.7159 20.9386C54.5452 20.278 53.6337 19.3331 52.9648 18.1039C52.2958 16.8831 51.9697 15.4616 51.9697 13.8477C51.9697 12.2339 52.2958 10.8207 52.9397 9.60825C53.5836 8.39578 54.495 7.45924 55.6573 6.80702C56.828 6.15479 58.1659 5.82031 59.6878 5.82031C61.2096 5.82031 62.4806 6.14643 63.6178 6.79029C64.7551 7.43416 65.6331 8.32052 66.2518 9.44938C66.8706 10.5782 67.18 11.8576 67.18 13.2791C67.18 13.7725 67.1549 14.2909 67.0964 14.8428L67.1048 14.8344ZM63.8603 10.1769C63.4255 9.4661 62.8318 8.92258 62.0793 8.55465C61.3267 8.18673 60.4989 8.00277 59.5874 8.00277C58.2746 8.00277 57.1625 8.42086 56.2427 9.25705C55.3228 10.0932 54.796 11.2472 54.6623 12.7356H64.5126C64.5126 11.7489 64.2952 10.896 63.8603 10.1852V10.1769Z" fill="#F2F2F2"/>
<path d="M73.7695 8.20355V17.4016C73.7695 18.1626 73.9284 18.6977 74.2545 19.0071C74.5806 19.3165 75.1409 19.4754 75.9352 19.4754H77.8418V21.6662H75.5088C74.0622 21.6662 72.9835 21.3317 72.2644 20.6711C71.5452 20.0105 71.1857 18.9151 71.1857 17.3933V8.19519H69.1621V6.0629H71.1857V2.13281H73.7779V6.0629H77.8501V8.19519H73.7779L73.7695 8.20355Z" fill="#F2F2F2"/>
<path d="M85.9022 6.68902C86.9307 6.10369 88.093 5.80266 89.4058 5.80266C90.8106 5.80266 92.0732 6.13714 93.1937 6.79773C94.3142 7.46668 95.2006 8.39485 95.8444 9.59896C96.4883 10.8031 96.8144 12.2079 96.8144 13.7966C96.8144 15.3854 96.4883 16.7818 95.8444 18.011C95.2006 19.2486 94.3142 20.2018 93.1854 20.8875C92.0565 21.5732 90.7939 21.916 89.4141 21.916C88.0344 21.916 86.8805 21.6234 85.8687 21.0297C84.8569 20.4443 84.0876 19.6918 83.5775 18.7803V21.6568H80.9854V0.601562H83.5775V8.97182C84.1127 8.04365 84.8904 7.28272 85.9105 6.69738L85.9022 6.68902ZM93.4529 10.7362C92.9763 9.86654 92.3408 9.19759 91.5297 8.74605C90.7186 8.29451 89.8322 8.06037 88.8706 8.06037C87.909 8.06037 87.0394 8.29451 86.2366 8.75441C85.4255 9.22268 84.7817 9.89163 84.2967 10.778C83.8117 11.6643 83.5692 12.6845 83.5692 13.8384C83.5692 14.9924 83.8117 16.046 84.2967 16.9323C84.7817 17.8187 85.4255 18.4877 86.2366 18.9559C87.0394 19.4242 87.9174 19.65 88.8706 19.65C89.8239 19.65 90.727 19.4158 91.5297 18.9559C92.3324 18.4877 92.9763 17.8187 93.4529 16.9323C93.9296 16.046 94.1637 15.0091 94.1637 13.8134C94.1637 12.6176 93.9296 11.6142 93.4529 10.7362Z" fill="#F2F2F2"/>
<path d="M100.318 3.01864C99.9749 2.67581 99.8076 2.25771 99.8076 1.76436C99.8076 1.27101 99.9749 0.852913 100.318 0.510076C100.661 0.167238 101.079 0 101.572 0C102.065 0 102.45 0.167238 102.784 0.510076C103.119 0.852913 103.286 1.27101 103.286 1.76436C103.286 2.25771 103.119 2.67581 102.784 3.01864C102.45 3.36148 102.049 3.52872 101.572 3.52872C101.095 3.52872 100.661 3.36148 100.318 3.01864ZM102.826 6.06237V21.6657H100.234V6.06237H102.826Z" fill="#F2F2F2"/>
<path d="M111.773 6.52155C112.617 6.0282 113.646 5.77734 114.867 5.77734V8.45315H114.181C111.28 8.45315 109.825 10.0252 109.825 13.1776V21.6649H107.232V6.06165H109.825V8.5953C110.276 7.70058 110.928 7.00654 111.773 6.51319V6.52155Z" fill="#F2F2F2"/>
<path d="M117.861 9.60732C118.505 8.40321 119.391 7.46668 120.52 6.80609C121.649 6.1455 122.92 5.81102 124.325 5.81102C125.537 5.81102 126.666 6.09533 127.711 6.64721C128.757 7.20746 129.551 7.94331 130.103 8.85475V0.601562H132.72V21.6735H130.103V18.7385C129.593 19.6667 128.832 20.436 127.828 21.0297C126.825 21.6317 125.646 21.9244 124.3 21.9244C122.953 21.9244 121.657 21.5816 120.528 20.8959C119.4 20.2102 118.513 19.257 117.869 18.0194C117.226 16.7818 116.899 15.377 116.899 13.805C116.899 12.233 117.226 10.8114 117.869 9.60732H117.861ZM129.392 10.7613C128.915 9.89163 128.28 9.22268 127.469 8.75441C126.658 8.28614 125.771 8.06037 124.81 8.06037C123.848 8.06037 122.962 8.28614 122.159 8.74605C121.356 9.20595 120.729 9.86654 120.253 10.7362C119.776 11.6058 119.542 12.6343 119.542 13.8134C119.542 14.9924 119.776 16.046 120.253 16.9323C120.729 17.8187 121.365 18.4877 122.159 18.9559C122.953 19.4242 123.84 19.65 124.81 19.65C125.78 19.65 126.666 19.4158 127.469 18.9559C128.272 18.4877 128.915 17.8187 129.392 16.9323C129.869 16.046 130.103 15.0175 130.103 13.8384C130.103 12.6594 129.869 11.6393 129.392 10.7613Z" fill="#F2F2F2"/>
<path d="M21.4651 0.568359C17.8193 0.902835 16.0047 3.00167 15.3191 4.06363L4.66602 22.5183H17.5182L30.1949 0.568359H21.4651Z" fill="#F68330"/>
<path d="M17.5265 22.5187L0 3.9302C0 3.9302 19.8177 -1.39633 21.7493 15.2188L17.5265 22.5187Z" fill="#F68330"/>
<path d="M14.9255 4.75055L9.54883 14.0657L17.5177 22.5196L21.7405 15.2029C21.0715 9.49174 18.287 6.37276 14.9255 4.74219" fill="#F35E32"/>
</g>
<defs>
<clipPath id="clip0_0_3">
<rect width="132.72" height="22.5186" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1,5 @@
<svg width="31" height="23" viewBox="0 0 31 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
</svg>

After

Width:  |  Height:  |  Size: 500 B

View File

@@ -0,0 +1,156 @@
import { cn } from "@/utils/helpers";
import { forwardRef } from "react";
type Variant =
| "default"
| "primary"
| "secondary"
| "secondaryLighter"
| "input"
| "dropdown"
| "dotted"
| "tertiary"
| "white"
| "outline"
| "danger-outline"
| "danger-text"
| "default-outline"
| "danger";
type Size = "xs" | "xs2" | "sm" | "md" | "lg";
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: Variant;
size?: Size;
rounded?: boolean;
border?: 0 | 1 | 2;
disabled?: boolean;
stopPropagation?: boolean;
}
const baseStyles = [
"relative cursor-pointer",
"text-sm focus:z-10 focus:ring-2 font-medium focus:outline-none whitespace-nowrap shadow-sm",
"inline-flex gap-2 items-center justify-center transition-colors focus:ring-offset-1",
"disabled:opacity-40 disabled:cursor-not-allowed disabled:text-nb-gray-300 ring-offset-neutral-950/50",
];
const variantStyles: Record<Variant, string[]> = {
default: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
"dark:focus:ring-zinc-800/50 dark:bg-nb-gray dark:text-gray-400 dark:border-gray-700/30 dark:hover:text-white dark:hover:bg-zinc-800/50",
],
primary: [
"dark:focus:ring-netbird-600/50 dark:ring-offset-neutral-950/50 enabled:dark:bg-netbird disabled:dark:bg-nb-gray-910 dark:text-gray-100 enabled:dark:hover:text-white enabled:dark:hover:bg-netbird-500/80",
"enabled:bg-netbird enabled:text-white enabled:focus:ring-netbird-400/50 enabled:hover:bg-netbird-500",
],
secondary: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20",
"dark:bg-nb-gray-920 dark:text-gray-400 dark:border-gray-700/40 dark:hover:text-white dark:hover:bg-nb-gray-910",
],
secondaryLighter: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20",
"dark:bg-nb-gray-900/70 dark:text-gray-400 dark:border-gray-700/70 dark:hover:text-white dark:hover:bg-nb-gray-800/60",
],
input: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-neutral-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20",
"dark:bg-nb-gray-900 dark:text-gray-400 dark:border-nb-gray-700 dark:hover:bg-nb-gray-900/80",
],
dropdown: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-neutral-200 text-gray-900",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20",
"dark:bg-nb-gray-900/40 dark:text-gray-400 dark:border-nb-gray-900 dark:hover:bg-nb-gray-900/50",
],
dotted: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900 border-dashed",
"dark:ring-offset-neutral-950/50 dark:focus:ring-neutral-500/20",
"dark:bg-nb-gray-900/30 dark:text-gray-400 dark:border-gray-500/40 dark:hover:text-white dark:hover:bg-zinc-800/50",
],
tertiary: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
"dark:focus:ring-zinc-800/50 dark:bg-white dark:text-gray-800 dark:border-gray-700/40 dark:hover:bg-neutral-200 disabled:dark:bg-nb-gray-920 disabled:dark:text-nb-gray-300",
],
white: [
"focus:ring-white/50 bg-white text-gray-800 border-white outline-none hover:bg-neutral-200 disabled:dark:bg-nb-gray-920 disabled:dark:text-nb-gray-300",
"disabled:dark:bg-nb-gray-900 disabled:dark:text-nb-gray-300 disabled:dark:border-nb-gray-900",
],
outline: [
"bg-white hover:text-black focus:ring-zinc-200/50 hover:bg-gray-100 border-gray-200 text-gray-900",
"dark:focus:ring-zinc-800/50 dark:bg-transparent dark:text-netbird dark:border-netbird dark:hover:bg-nb-gray-900/30",
],
"danger-outline": [
"enabled:dark:focus:ring-red-800/20 enabled:dark:focus:bg-red-950/40 enabled:hover:dark:bg-red-950/50 enabled:dark:hover:border-red-800/50 dark:bg-transparent dark:text-red-500",
],
"danger-text": [
"dark:bg-transparent dark:text-red-500 dark:hover:text-red-600 dark:border-transparent !px-0 !shadow-none !py-0 focus:ring-red-500/30 dark:ring-offset-neutral-950/50",
],
"default-outline": [
"dark:ring-offset-nb-gray-950/50 dark:focus:ring-nb-gray-500/20",
"dark:bg-transparent dark:text-nb-gray-400 dark:border-transparent dark:hover:text-white dark:hover:bg-nb-gray-900/30 dark:hover:border-nb-gray-800/50",
"data-[state=open]:dark:text-white data-[state=open]:dark:bg-nb-gray-900/30 data-[state=open]:dark:border-nb-gray-800/50",
],
danger: [
"dark:focus:ring-red-700/20 dark:focus:bg-red-700 hover:dark:bg-red-700 dark:hover:border-red-800/50 dark:bg-red-600 dark:text-red-100",
],
};
const sizeStyles: Record<Size, string> = {
xs: "text-xs py-2 px-4",
xs2: "text-[0.78rem] py-2 px-4",
sm: "text-sm py-2.5 px-4",
md: "text-sm py-2.5 px-4",
lg: "text-base py-2.5 px-4",
};
const borderStyles: Record<0 | 1 | 2, string> = {
0: "border",
1: "border border-transparent",
2: "border border-t-0 border-b-0",
};
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = "default",
rounded = true,
border = 1,
size = "md",
stopPropagation = true,
className,
onClick,
children,
...props
},
ref
) => {
return (
<button
type="button"
{...props}
ref={ref}
className={cn(
baseStyles,
variantStyles[variant],
sizeStyles[size],
borderStyles[border ? 1 : 0],
rounded && "rounded-md",
className
)}
onClick={(e) => {
if (stopPropagation) e.stopPropagation();
onClick?.(e);
}}
>
{children}
</button>
);
}
);
Button.displayName = "Button";
export default Button;

View File

@@ -0,0 +1,23 @@
import { cn } from "@/utils/helpers";
import { GradientFadedBackground } from "@/components/GradientFadedBackground";
export const Card = ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => {
return (
<div
className={cn(
"px-6 sm:px-10 py-10 pt-8",
"bg-nb-gray-940 border border-nb-gray-910 rounded-lg relative",
className
)}
>
<GradientFadedBackground />
{children}
</div>
);
};

View File

@@ -0,0 +1,14 @@
import { cn } from "@/utils/helpers";
type Props = {
children: React.ReactNode;
className?: string;
};
export function Description({ children, className }: Props) {
return (
<div className={cn("text-sm text-nb-gray-300 font-light mt-2 block text-center z-10 relative", className)}>
{children}
</div>
);
}

View File

@@ -0,0 +1,7 @@
export const ErrorMessage = ({ error }: { error?: string }) => {
return (
<div className="text-red-400 bg-red-800/20 border border-red-800/50 rounded-lg px-4 py-3 whitespace-break-spaces text-sm">
{error}
</div>
);
};

View File

@@ -0,0 +1,22 @@
import { cn } from "@/utils/helpers";
type Props = {
className?: string;
};
export const GradientFadedBackground = ({ className }: Props) => {
return (
<div
className={cn(
"h-full w-full absolute left-0 top-0 rounded-md overflow-hidden z-0 pointer-events-none",
className
)}
>
<div
className={
"bg-linear-to-b from-nb-gray-900/10 via-transparent to-transparent w-full h-full rounded-md"
}
></div>
</div>
);
};

View File

@@ -0,0 +1,19 @@
import { cn } from "@/utils/helpers";
interface HelpTextProps {
children?: React.ReactNode;
className?: string;
}
export default function HelpText({ children, className }: HelpTextProps) {
return (
<span
className={cn(
"text-[.8rem] text-nb-gray-300 block font-light tracking-wide",
className
)}
>
{children}
</span>
);
}

View File

@@ -0,0 +1,137 @@
import { cn } from "@/utils/helpers";
import { Eye, EyeOff } from "lucide-react";
import * as React from "react";
import { useState } from "react";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
customPrefix?: React.ReactNode;
customSuffix?: React.ReactNode;
maxWidthClass?: string;
icon?: React.ReactNode;
error?: string;
prefixClassName?: string;
showPasswordToggle?: boolean;
variant?: "default" | "darker";
}
const variantStyles = {
default: [
"bg-nb-gray-900 placeholder:text-neutral-400/70 border-nb-gray-700",
"ring-offset-neutral-950/50 focus-visible:ring-neutral-500/20",
],
darker: [
"bg-nb-gray-920 placeholder:text-neutral-400/70 border-nb-gray-800",
"ring-offset-neutral-950/50 focus-visible:ring-neutral-500/20",
],
error: [
"bg-nb-gray-900 placeholder:text-neutral-400/70 border-red-500 text-red-500",
"ring-offset-red-500/10 focus-visible:ring-red-500/10",
],
};
const prefixSuffixStyles = {
default: "bg-nb-gray-900 border-nb-gray-700 text-nb-gray-300",
error: "bg-nb-gray-900 border-red-500 text-nb-gray-300 text-red-500",
};
const Input = React.forwardRef<HTMLInputElement, InputProps>(
(
{
className,
type,
customSuffix,
customPrefix,
icon,
maxWidthClass = "",
error,
variant = "default",
prefixClassName,
showPasswordToggle = false,
...props
},
ref
) => {
const [showPassword, setShowPassword] = useState(false);
const isPasswordType = type === "password";
const inputType = isPasswordType && showPassword ? "text" : type;
const passwordToggle =
isPasswordType && showPasswordToggle ? (
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="hover:text-white transition-all"
aria-label="Toggle password visibility"
>
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
</button>
) : null;
const suffix = passwordToggle || customSuffix;
const activeVariant = error ? "error" : variant;
return (
<>
<div className={cn("flex relative h-[42px]", maxWidthClass)}>
{customPrefix && (
<div
className={cn(
prefixSuffixStyles[error ? "error" : "default"],
"flex h-[42px] w-auto rounded-l-md px-3 py-2 text-sm",
"border items-center whitespace-nowrap",
props.disabled && "opacity-40",
prefixClassName
)}
>
{customPrefix}
</div>
)}
<div
className={cn(
"absolute left-0 top-0 h-full flex items-center text-xs text-nb-gray-300 pl-3 leading-[0]",
props.disabled && "opacity-40"
)}
>
{icon}
</div>
<input
type={inputType}
ref={ref}
{...props}
className={cn(
variantStyles[activeVariant],
"flex h-[42px] w-full rounded-md px-3 py-2 text-sm",
"file:bg-transparent file:text-sm file:font-medium file:border-0",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"disabled:cursor-not-allowed disabled:opacity-40",
"border",
customPrefix && "!border-l-0 !rounded-l-none",
suffix && "!pr-16",
icon && "!pl-10",
className
)}
/>
<div
className={cn(
"absolute right-0 top-0 h-full flex items-center text-xs text-nb-gray-300 pr-4 leading-[0] select-none",
props.disabled && "opacity-30"
)}
>
{suffix}
</div>
</div>
{error && (
<p className="text-xs text-red-500 mt-2">{error}</p>
)}
</>
);
}
);
Input.displayName = "Input";
export { Input };

View File

@@ -0,0 +1,18 @@
import { cn } from "@/utils/helpers";
type LabelProps = React.LabelHTMLAttributes<HTMLLabelElement>;
export function Label({ className, ...props }: LabelProps) {
return (
<label
className={cn(
"text-sm font-medium tracking-wider leading-none",
"peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
"mb-2.5 inline-block text-nb-gray-200",
"flex items-center gap-2 select-none",
className
)}
{...props}
/>
);
}

View File

@@ -0,0 +1,46 @@
import { cn } from "@/utils/helpers";
import netbirdFull from "@/assets/netbird-full.svg";
import netbirdMark from "@/assets/netbird.svg";
type Props = {
size?: "small" | "default" | "large";
mobile?: boolean;
};
const sizes = {
small: {
desktop: 14,
mobile: 20,
},
default: {
desktop: 22,
mobile: 30,
},
large: {
desktop: 24,
mobile: 40,
},
};
export const NetBirdLogo = ({ size = "default", mobile = true }: Props) => {
return (
<>
<img
src={netbirdFull}
height={sizes[size].desktop}
style={{ height: sizes[size].desktop }}
alt="NetBird Logo"
className={cn(mobile && "hidden md:block", "group-hover:opacity-80 transition-all")}
/>
{mobile && (
<img
src={netbirdMark}
width={sizes[size].mobile}
style={{ width: sizes[size].mobile }}
alt="NetBird Logo"
className={cn(mobile && "md:hidden ml-4")}
/>
)}
</>
);
};

View File

@@ -0,0 +1,107 @@
import { cn } from "@/utils/helpers";
import React, {
useRef,
type KeyboardEvent,
type ClipboardEvent,
forwardRef,
useImperativeHandle,
} from "react";
export interface PinCodeInputRef {
focus: () => void;
}
interface Props {
value: string;
onChange: (value: string) => void;
length?: number;
disabled?: boolean;
className?: string;
autoFocus?: boolean;
}
const PinCodeInput = forwardRef<PinCodeInputRef, Props>(function PinCodeInput(
{ value, onChange, length = 6, disabled = false, className, autoFocus = false },
ref,
) {
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
useImperativeHandle(ref, () => ({
focus: () => {
inputRefs.current[0]?.focus();
},
}));
const digits = value.split("").concat(Array(length).fill("")).slice(0, length);
const handleChange = (index: number, digit: string) => {
if (!/^\d*$/.test(digit)) return;
const newDigits = [...digits];
newDigits[index] = digit.slice(-1);
const newValue = newDigits.join("").replace(/\s/g, "");
onChange(newValue);
if (digit && index < length - 1) {
inputRefs.current[index + 1]?.focus();
}
};
const handleKeyDown = (index: number, e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Backspace" && !digits[index] && index > 0) {
inputRefs.current[index - 1]?.focus();
}
if (e.key === "ArrowLeft" && index > 0) {
inputRefs.current[index - 1]?.focus();
}
if (e.key === "ArrowRight" && index < length - 1) {
inputRefs.current[index + 1]?.focus();
}
};
const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
e.preventDefault();
const pastedData = e.clipboardData.getData("text").replace(/\D/g, "").slice(0, length);
onChange(pastedData);
const nextIndex = Math.min(pastedData.length, length - 1);
inputRefs.current[nextIndex]?.focus();
};
const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => {
e.target.select();
};
return (
<div className={cn("flex gap-2 w-full min-w-0", className)}>
{digits.map((digit, index) => (
<input
key={index}
ref={(el) => {
inputRefs.current[index] = el;
}}
type="text"
inputMode="numeric"
maxLength={1}
value={digit}
onChange={(e) => handleChange(index, e.target.value)}
onKeyDown={(e) => handleKeyDown(index, e)}
onPaste={handlePaste}
onFocus={handleFocus}
disabled={disabled}
autoFocus={autoFocus && index === 0}
className={cn(
"flex-1 min-w-0 h-[42px] text-center text-sm rounded-md",
"dark:bg-nb-gray-900 border dark:border-nb-gray-700",
"dark:placeholder:text-neutral-400/70",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2",
"ring-offset-neutral-200/20 dark:ring-offset-neutral-950/50 dark:focus-visible:ring-neutral-500/20",
"disabled:cursor-not-allowed disabled:opacity-40"
)}
/>
))}
</div>
);
});
export default PinCodeInput;

View File

@@ -0,0 +1,12 @@
import { NetBirdLogo } from "./NetBirdLogo";
export function PoweredByNetBird() {
return (
<div className="flex items-center justify-center mt-8 gap-2 group cursor-pointer">
<span className="text-sm text-nb-gray-400 font-light text-center group-hover:opacity-80 transition-all">
Powered by
</span>
<NetBirdLogo size="small" mobile={false} />
</div>
);
}

View File

@@ -0,0 +1,137 @@
import { cn } from "@/utils/helpers";
import { useState } from "react";
import { TabContext, useTabContext } from "./TabContext";
type TabsProps = {
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
children:
| React.ReactNode
| ((context: { value: string; onChange: (value: string) => void }) => React.ReactNode);
};
function SegmentedTabs({ value, defaultValue, onChange, children }: TabsProps) {
const [internalValue, setInternalValue] = useState(defaultValue || "");
const currentValue = value !== undefined ? value : internalValue;
const handleChange = (newValue: string) => {
if (value === undefined) {
setInternalValue(newValue);
}
onChange?.(newValue);
};
return (
<TabContext.Provider value={{ value: currentValue, onChange: handleChange }}>
<div>
{typeof children === "function"
? children({ value: currentValue, onChange: handleChange })
: children}
</div>
</TabContext.Provider>
);
}
function List({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<div
role="tablist"
className={cn(
"bg-nb-gray-930/70 p-1.5 flex justify-center gap-1 border-nb-gray-900",
className
)}
>
{children}
</div>
);
}
function Trigger({
children,
value,
disabled = false,
className,
selected,
onClick,
}: {
children: React.ReactNode;
value: string;
disabled?: boolean;
className?: string;
selected?: boolean;
onClick?: () => void;
}) {
const context = useTabContext();
const isSelected = selected !== undefined ? selected : value === context.value;
const handleClick = () => {
context.onChange(value);
onClick?.();
};
return (
<button
role="tab"
type="button"
disabled={disabled}
aria-selected={isSelected}
onClick={handleClick}
className={cn(
"px-4 py-2 text-sm rounded-md w-full transition-all cursor-pointer",
disabled && "opacity-30 cursor-not-allowed",
isSelected
? "bg-nb-gray-900 text-white"
: disabled
? ""
: "text-nb-gray-400 hover:bg-nb-gray-900/50",
className
)}
>
<div className="flex items-center w-full justify-center gap-2">
{children}
</div>
</button>
);
}
function Content({
children,
value,
className,
visible,
}: {
children: React.ReactNode;
value: string;
className?: string;
visible?: boolean;
}) {
const context = useTabContext();
const isVisible = visible !== undefined ? visible : value === context.value;
if (!isVisible) return null;
return (
<div
role="tabpanel"
className={cn(
"bg-nb-gray-930/70 px-4 pt-4 pb-5 rounded-b-md border border-t-0 border-nb-gray-900",
className
)}
>
{children}
</div>
);
}
SegmentedTabs.List = List;
SegmentedTabs.Trigger = Trigger;
SegmentedTabs.Content = Content;
export { SegmentedTabs };

View File

@@ -0,0 +1,10 @@
export const Separator = () => {
return (
<div className="flex items-center justify-center relative my-4">
<span className="bg-nb-gray-940 relative z-10 px-4 text-xs text-nb-gray-400 font-medium">
OR
</span>
<span className="h-px bg-nb-gray-900 w-full absolute z-0" />
</div>
);
};

View File

@@ -0,0 +1,13 @@
import { createContext, useContext } from "react";
type TabContextValue = {
value: string;
onChange: (value: string) => void;
};
export const TabContext = createContext<TabContextValue>({
value: "",
onChange: () => {},
});
export const useTabContext = () => useContext(TabContext);

View File

@@ -0,0 +1,14 @@
import { cn } from "@/utils/helpers";
type Props = {
children: React.ReactNode;
className?: string;
};
export function Title({ children, className }: Props) {
return (
<h1 className={cn("text-xl! text-center z-10 relative", className)}>
{children}
</h1>
);
}

17
proxy/web/src/data.ts Normal file
View File

@@ -0,0 +1,17 @@
// Auth method types matching Go
export type AuthMethod = 'pin' | 'password' | 'oidc' | "link"
// Data injected by Go templates
export interface Data {
methods?: Partial<Record<AuthMethod, string>>
}
declare global {
interface Window {
__DATA__?: Data
}
}
export function getData(): Data {
return window.__DATA__ ?? {}
}

214
proxy/web/src/index.css Normal file
View File

@@ -0,0 +1,214 @@
@import "tailwindcss";
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("./assets/fonts/Inter-VariableFont_opsz,wght.ttf") format("truetype");
}
@font-face {
font-family: "Inter";
font-style: italic;
font-weight: 100 900;
font-display: swap;
src: url("./assets/fonts/Inter-Italic-VariableFont_opsz,wght.ttf") format("truetype");
}
@theme {
/* Gray */
--color-gray-50: #F9FAFB;
--color-gray-100: #F3F4F6;
--color-gray-200: #E5E7EB;
--color-gray-300: #D1D5DB;
--color-gray-400: #9CA3AF;
--color-gray-500: #6B7280;
--color-gray-600: #4B5563;
--color-gray-700: #374151;
--color-gray-800: #1F2937;
--color-gray-900: #111827;
/* Red */
--color-red-50: #FDF2F2;
--color-red-100: #FDE8E8;
--color-red-200: #FBD5D5;
--color-red-300: #F8B4B4;
--color-red-400: #F98080;
--color-red-500: #F05252;
--color-red-600: #E02424;
--color-red-700: #C81E1E;
--color-red-800: #9B1C1C;
--color-red-900: #771D1D;
/* Yellow */
--color-yellow-50: #FDFDEA;
--color-yellow-100: #FDF6B2;
--color-yellow-200: #FCE96A;
--color-yellow-300: #FACA15;
--color-yellow-400: #E3A008;
--color-yellow-500: #C27803;
--color-yellow-600: #9F580A;
--color-yellow-700: #8E4B10;
--color-yellow-800: #723B13;
--color-yellow-900: #633112;
/* Green */
--color-green-50: #F3FAF7;
--color-green-100: #DEF7EC;
--color-green-200: #BCF0DA;
--color-green-300: #84E1BC;
--color-green-400: #31C48D;
--color-green-500: #0E9F6E;
--color-green-600: #057A55;
--color-green-700: #046C4E;
--color-green-800: #03543F;
--color-green-900: #014737;
/* Blue */
--color-blue-50: #EBF5FF;
--color-blue-100: #E1EFFE;
--color-blue-200: #C3DDFD;
--color-blue-300: #A4CAFE;
--color-blue-400: #76A9FA;
--color-blue-500: #3F83F8;
--color-blue-600: #1C64F2;
--color-blue-700: #1A56DB;
--color-blue-800: #1E429F;
--color-blue-900: #233876;
/* Indigo */
--color-indigo-50: #F0F5FF;
--color-indigo-100: #E5EDFF;
--color-indigo-200: #CDDBFE;
--color-indigo-300: #B4C6FC;
--color-indigo-400: #8DA2FB;
--color-indigo-500: #6875F5;
--color-indigo-600: #5850EC;
--color-indigo-700: #5145CD;
--color-indigo-800: #42389D;
--color-indigo-900: #362F78;
/* Purple */
--color-purple-50: #F6F5FF;
--color-purple-100: #EDEBFE;
--color-purple-200: #DCD7FE;
--color-purple-300: #CABFFD;
--color-purple-400: #AC94FA;
--color-purple-500: #9061F9;
--color-purple-600: #7E3AF2;
--color-purple-700: #6C2BD9;
--color-purple-800: #5521B5;
--color-purple-900: #4A1D96;
/* Pink */
--color-pink-50: #FDF2F8;
--color-pink-100: #FCE8F3;
--color-pink-200: #FAD1E8;
--color-pink-300: #F8B4D9;
--color-pink-400: #F17EB8;
--color-pink-500: #E74694;
--color-pink-600: #D61F69;
--color-pink-700: #BF125D;
--color-pink-800: #99154B;
--color-pink-900: #751A3D;
/* NetBird Gray */
--color-nb-gray: #181A1D;
--color-nb-gray-50: #f4f6f7;
--color-nb-gray-100: #e4e7e9;
--color-nb-gray-200: #cbd2d6;
--color-nb-gray-250: #b7c0c6;
--color-nb-gray-300: #aab4bd;
--color-nb-gray-350: #8f9ca8;
--color-nb-gray-400: #7c8994;
--color-nb-gray-500: #616e79;
--color-nb-gray-600: #535d67;
--color-nb-gray-700: #474e57;
--color-nb-gray-800: #3f444b;
--color-nb-gray-850: #363b40;
--color-nb-gray-900: #32363D;
--color-nb-gray-910: #2b2f33;
--color-nb-gray-920: #25282d;
--color-nb-gray-925: #1e2123;
--color-nb-gray-930: #25282c;
--color-nb-gray-935: #1f2124;
--color-nb-gray-940: #1c1e21;
--color-nb-gray-950: #181a1d;
--color-nb-gray-960: #15171a;
/* NetBird Orange */
--color-netbird: #f68330;
--color-netbird-50: #fff6ed;
--color-netbird-100: #feecd6;
--color-netbird-150: #ffdfb8;
--color-netbird-200: #ffd4a6;
--color-netbird-300: #fab677;
--color-netbird-400: #f68330;
--color-netbird-500: #f46d1b;
--color-netbird-600: #e55311;
--color-netbird-700: #be3e10;
--color-netbird-800: #973215;
--color-netbird-900: #7a2b14;
--color-netbird-950: #421308;
/* NetBird Blue */
--color-nb-blue: #31e4f5;
--color-nb-blue-50: #ebffff;
--color-nb-blue-100: #cefdff;
--color-nb-blue-200: #a2f9ff;
--color-nb-blue-300: #63f2fd;
--color-nb-blue-400: #31e4f5;
--color-nb-blue-500: #00c4da;
--color-nb-blue-600: #039cb7;
--color-nb-blue-700: #0a7c94;
--color-nb-blue-800: #126478;
--color-nb-blue-900: #145365;
--color-nb-blue-950: #063746;
}
:root {
--nb-bg: #18191d;
--nb-card-bg: #1b1f22;
--nb-border: rgba(50, 54, 61, 0.5);
--nb-text: #e4e7e9;
--nb-text-muted: rgba(167, 177, 185, 0.8);
--nb-primary: #f68330;
--nb-primary-hover: #e5722a;
--nb-input-bg: rgba(63, 68, 75, 0.5);
--nb-input-border: rgba(63, 68, 75, 0.8);
--nb-error-bg: rgba(153, 27, 27, 0.2);
--nb-error-border: rgba(153, 27, 27, 0.5);
--nb-error-text: #f87171;
}
html {
color-scheme: dark;
}
html{
@apply bg-nb-gray;
}
html.dark,
:root {
color-scheme: dark;
}
body {
font-family: "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
}
h1 {
@apply text-2xl font-medium text-gray-700 dark:text-nb-gray-100 my-1;
}
h2 {
@apply text-xl font-medium text-gray-700 dark:text-nb-gray-100 my-1;
}
p {
@apply font-light tracking-wide text-gray-700 dark:text-zinc-50 text-sm;
}
[placeholder] {
text-overflow: ellipsis;
}

10
proxy/web/src/main.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

6
proxy/web/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="vite/client" />
declare module "*.svg" {
const content: string;
export default content;
}

22
proxy/web/tsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src", "vite.config.ts"]
}

24
proxy/web/vite.config.ts Normal file
View File

@@ -0,0 +1,24 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3031,
},
preview: {
port: 3031,
},
build: {
outDir: 'dist',
assetsDir: 'assets',
cssCodeSplit: false,
},
})

105
proxy/web/web.go Normal file
View File

@@ -0,0 +1,105 @@
package web
import (
"bytes"
"embed"
"encoding/json"
"html/template"
"io/fs"
"net/http"
"path/filepath"
"strings"
)
//go:embed dist/*
var files embed.FS
var (
webFS fs.FS
tmpl *template.Template
initErr error
)
func init() {
webFS, initErr = fs.Sub(files, "dist")
if initErr != nil {
return
}
var indexHTML []byte
indexHTML, initErr = fs.ReadFile(webFS, "index.html")
if initErr != nil {
return
}
tmpl, initErr = template.New("index").Parse(string(indexHTML))
}
// ServeHTTP serves the web UI. For static assets it serves them directly,
// for other paths it renders the page with the provided data.
func ServeHTTP(w http.ResponseWriter, r *http.Request, data any) {
if initErr != nil {
http.Error(w, initErr.Error(), http.StatusInternalServerError)
return
}
path := r.URL.Path
// Serve robots.txt
if path == "/robots.txt" {
content, err := fs.ReadFile(webFS, "robots.txt")
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "text/plain")
w.Write(content)
return
}
// Serve static assets directly
if strings.HasPrefix(path, "/assets/") {
filePath := strings.TrimPrefix(path, "/")
content, err := fs.ReadFile(webFS, filePath)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
switch filepath.Ext(filePath) {
case ".js":
w.Header().Set("Content-Type", "application/javascript")
case ".css":
w.Header().Set("Content-Type", "text/css")
case ".svg":
w.Header().Set("Content-Type", "image/svg+xml")
case ".ttf":
w.Header().Set("Content-Type", "font/ttf")
case ".woff":
w.Header().Set("Content-Type", "font/woff")
case ".woff2":
w.Header().Set("Content-Type", "font/woff2")
case ".ico":
w.Header().Set("Content-Type", "image/x-icon")
}
w.Write(content)
return
}
// Render the page with data
dataJSON, _ := json.Marshal(data)
var buf bytes.Buffer
if err := tmpl.Execute(&buf, struct {
Data template.JS
}{
Data: template.JS(dataJSON),
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html")
w.Write(buf.Bytes())
}

View File

@@ -2855,6 +2855,8 @@ components:
description: Whether the reverse proxy is enabled
auth:
$ref: '#/components/schemas/ReverseProxyAuthConfig'
meta:
$ref: '#/components/schemas/ReverseProxyMeta'
required:
- id
- name
@@ -2862,6 +2864,34 @@ components:
- targets
- enabled
- auth
- meta
ReverseProxyMeta:
type: object
properties:
created_at:
type: string
format: date-time
description: Timestamp when the reverse proxy was created
example: "2024-02-03T10:30:00Z"
certificate_issued_at:
type: string
format: date-time
description: Timestamp when the certificate was issued (empty if not yet issued)
example: "2024-02-03T10:35:00Z"
status:
type: string
enum:
- pending
- active
- tunnel_not_created
- certificate_pending
- certificate_failed
- error
description: Current status of the reverse proxy
example: "active"
required:
- created_at
- status
ReverseProxyRequest:
type: object
properties:

View File

@@ -275,6 +275,16 @@ const (
ReverseProxyDomainTypeFree ReverseProxyDomainType = "free"
)
// Defines values for ReverseProxyMetaStatus.
const (
ReverseProxyMetaStatusActive ReverseProxyMetaStatus = "active"
ReverseProxyMetaStatusCertificateFailed ReverseProxyMetaStatus = "certificate_failed"
ReverseProxyMetaStatusCertificatePending ReverseProxyMetaStatus = "certificate_pending"
ReverseProxyMetaStatusError ReverseProxyMetaStatus = "error"
ReverseProxyMetaStatusPending ReverseProxyMetaStatus = "pending"
ReverseProxyMetaStatusTunnelNotCreated ReverseProxyMetaStatus = "tunnel_not_created"
)
// Defines values for ReverseProxyTargetProtocol.
const (
ReverseProxyTargetProtocolHttp ReverseProxyTargetProtocol = "http"
@@ -1958,7 +1968,8 @@ type ReverseProxy struct {
Enabled bool `json:"enabled"`
// Id Reverse proxy ID
Id string `json:"id"`
Id string `json:"id"`
Meta ReverseProxyMeta `json:"meta"`
// Name Reverse proxy name
Name string `json:"name"`
@@ -1999,6 +2010,21 @@ type ReverseProxyDomainRequest struct {
// ReverseProxyDomainType Type of Reverse Proxy Domain
type ReverseProxyDomainType string
// ReverseProxyMeta defines model for ReverseProxyMeta.
type ReverseProxyMeta struct {
// CertificateIssuedAt Timestamp when the certificate was issued (empty if not yet issued)
CertificateIssuedAt *time.Time `json:"certificate_issued_at,omitempty"`
// CreatedAt Timestamp when the reverse proxy was created
CreatedAt time.Time `json:"created_at"`
// Status Current status of the reverse proxy
Status ReverseProxyMetaStatus `json:"status"`
}
// ReverseProxyMetaStatus Current status of the reverse proxy
type ReverseProxyMetaStatus string
// ReverseProxyRequest defines model for ReverseProxyRequest.
type ReverseProxyRequest struct {
Auth ReverseProxyAuthConfig `json:"auth"`

View File

@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.26.0
// protoc v3.21.12
// protoc v6.33.0
// source: proxy_service.proto
package proto
@@ -70,6 +70,64 @@ func (ProxyMappingUpdateType) EnumDescriptor() ([]byte, []int) {
return file_proxy_service_proto_rawDescGZIP(), []int{0}
}
type ProxyStatus int32
const (
ProxyStatus_PROXY_STATUS_PENDING ProxyStatus = 0
ProxyStatus_PROXY_STATUS_ACTIVE ProxyStatus = 1
ProxyStatus_PROXY_STATUS_TUNNEL_NOT_CREATED ProxyStatus = 2
ProxyStatus_PROXY_STATUS_CERTIFICATE_PENDING ProxyStatus = 3
ProxyStatus_PROXY_STATUS_CERTIFICATE_FAILED ProxyStatus = 4
ProxyStatus_PROXY_STATUS_ERROR ProxyStatus = 5
)
// Enum value maps for ProxyStatus.
var (
ProxyStatus_name = map[int32]string{
0: "PROXY_STATUS_PENDING",
1: "PROXY_STATUS_ACTIVE",
2: "PROXY_STATUS_TUNNEL_NOT_CREATED",
3: "PROXY_STATUS_CERTIFICATE_PENDING",
4: "PROXY_STATUS_CERTIFICATE_FAILED",
5: "PROXY_STATUS_ERROR",
}
ProxyStatus_value = map[string]int32{
"PROXY_STATUS_PENDING": 0,
"PROXY_STATUS_ACTIVE": 1,
"PROXY_STATUS_TUNNEL_NOT_CREATED": 2,
"PROXY_STATUS_CERTIFICATE_PENDING": 3,
"PROXY_STATUS_CERTIFICATE_FAILED": 4,
"PROXY_STATUS_ERROR": 5,
}
)
func (x ProxyStatus) Enum() *ProxyStatus {
p := new(ProxyStatus)
*p = x
return p
}
func (x ProxyStatus) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (ProxyStatus) Descriptor() protoreflect.EnumDescriptor {
return file_proxy_service_proto_enumTypes[1].Descriptor()
}
func (ProxyStatus) Type() protoreflect.EnumType {
return &file_proxy_service_proto_enumTypes[1]
}
func (x ProxyStatus) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use ProxyStatus.Descriptor instead.
func (ProxyStatus) EnumDescriptor() ([]byte, []int) {
return file_proxy_service_proto_rawDescGZIP(), []int{1}
}
// GetMappingUpdateRequest is sent to initialise a mapping stream.
type GetMappingUpdateRequest struct {
state protoimpl.MessageState
@@ -323,10 +381,7 @@ type OIDC struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Issuer string `protobuf:"bytes,1,opt,name=issuer,proto3" json:"issuer,omitempty"`
Audiences []string `protobuf:"bytes,2,rep,name=audiences,proto3" json:"audiences,omitempty"`
KeysLocation string `protobuf:"bytes,3,opt,name=keys_location,json=keysLocation,proto3" json:"keys_location,omitempty"`
MaxTokenAge int64 `protobuf:"varint,4,opt,name=max_token_age,json=maxTokenAge,proto3" json:"max_token_age,omitempty"`
DistributionGroups []string `protobuf:"bytes,1,rep,name=distribution_groups,json=distributionGroups,proto3" json:"distribution_groups,omitempty"`
}
func (x *OIDC) Reset() {
@@ -361,34 +416,13 @@ func (*OIDC) Descriptor() ([]byte, []int) {
return file_proxy_service_proto_rawDescGZIP(), []int{4}
}
func (x *OIDC) GetIssuer() string {
func (x *OIDC) GetDistributionGroups() []string {
if x != nil {
return x.Issuer
}
return ""
}
func (x *OIDC) GetAudiences() []string {
if x != nil {
return x.Audiences
return x.DistributionGroups
}
return nil
}
func (x *OIDC) GetKeysLocation() string {
if x != nil {
return x.KeysLocation
}
return ""
}
func (x *OIDC) GetMaxTokenAge() int64 {
if x != nil {
return x.MaxTokenAge
}
return 0
}
type ProxyMapping struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1021,18 +1055,21 @@ func (x *AuthenticateResponse) GetSuccess() bool {
return false
}
type GetOIDCURLRequest struct {
// SendStatusUpdateRequest is sent by the proxy to update its status
type SendStatusUpdateRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
AccountId string `protobuf:"bytes,2,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"`
RedirectUrl string `protobuf:"bytes,3,opt,name=redirect_url,json=redirectUrl,proto3" json:"redirect_url,omitempty"`
ReverseProxyId string `protobuf:"bytes,1,opt,name=reverse_proxy_id,json=reverseProxyId,proto3" json:"reverse_proxy_id,omitempty"`
AccountId string `protobuf:"bytes,2,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"`
Status ProxyStatus `protobuf:"varint,3,opt,name=status,proto3,enum=management.ProxyStatus" json:"status,omitempty"`
CertificateIssued bool `protobuf:"varint,4,opt,name=certificate_issued,json=certificateIssued,proto3" json:"certificate_issued,omitempty"`
ErrorMessage *string `protobuf:"bytes,5,opt,name=error_message,json=errorMessage,proto3,oneof" json:"error_message,omitempty"`
}
func (x *GetOIDCURLRequest) Reset() {
*x = GetOIDCURLRequest{}
func (x *SendStatusUpdateRequest) Reset() {
*x = SendStatusUpdateRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_proxy_service_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -1040,13 +1077,13 @@ func (x *GetOIDCURLRequest) Reset() {
}
}
func (x *GetOIDCURLRequest) String() string {
func (x *SendStatusUpdateRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetOIDCURLRequest) ProtoMessage() {}
func (*SendStatusUpdateRequest) ProtoMessage() {}
func (x *GetOIDCURLRequest) ProtoReflect() protoreflect.Message {
func (x *SendStatusUpdateRequest) ProtoReflect() protoreflect.Message {
mi := &file_proxy_service_proto_msgTypes[14]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -1058,42 +1095,55 @@ func (x *GetOIDCURLRequest) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use GetOIDCURLRequest.ProtoReflect.Descriptor instead.
func (*GetOIDCURLRequest) Descriptor() ([]byte, []int) {
// Deprecated: Use SendStatusUpdateRequest.ProtoReflect.Descriptor instead.
func (*SendStatusUpdateRequest) Descriptor() ([]byte, []int) {
return file_proxy_service_proto_rawDescGZIP(), []int{14}
}
func (x *GetOIDCURLRequest) GetId() string {
func (x *SendStatusUpdateRequest) GetReverseProxyId() string {
if x != nil {
return x.Id
return x.ReverseProxyId
}
return ""
}
func (x *GetOIDCURLRequest) GetAccountId() string {
func (x *SendStatusUpdateRequest) GetAccountId() string {
if x != nil {
return x.AccountId
}
return ""
}
func (x *GetOIDCURLRequest) GetRedirectUrl() string {
func (x *SendStatusUpdateRequest) GetStatus() ProxyStatus {
if x != nil {
return x.RedirectUrl
return x.Status
}
return ProxyStatus_PROXY_STATUS_PENDING
}
func (x *SendStatusUpdateRequest) GetCertificateIssued() bool {
if x != nil {
return x.CertificateIssued
}
return false
}
func (x *SendStatusUpdateRequest) GetErrorMessage() string {
if x != nil && x.ErrorMessage != nil {
return *x.ErrorMessage
}
return ""
}
type GetOIDCURLResponse struct {
// SendStatusUpdateResponse is intentionally empty to allow for future expansion
type SendStatusUpdateResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
}
func (x *GetOIDCURLResponse) Reset() {
*x = GetOIDCURLResponse{}
func (x *SendStatusUpdateResponse) Reset() {
*x = SendStatusUpdateResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_proxy_service_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -1101,13 +1151,13 @@ func (x *GetOIDCURLResponse) Reset() {
}
}
func (x *GetOIDCURLResponse) String() string {
func (x *SendStatusUpdateResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetOIDCURLResponse) ProtoMessage() {}
func (*SendStatusUpdateResponse) ProtoMessage() {}
func (x *GetOIDCURLResponse) ProtoReflect() protoreflect.Message {
func (x *SendStatusUpdateResponse) ProtoReflect() protoreflect.Message {
mi := &file_proxy_service_proto_msgTypes[15]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
@@ -1119,18 +1169,11 @@ func (x *GetOIDCURLResponse) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x)
}
// Deprecated: Use GetOIDCURLResponse.ProtoReflect.Descriptor instead.
func (*GetOIDCURLResponse) Descriptor() ([]byte, []int) {
// Deprecated: Use SendStatusUpdateResponse.ProtoReflect.Descriptor instead.
func (*SendStatusUpdateResponse) Descriptor() ([]byte, []int) {
return file_proxy_service_proto_rawDescGZIP(), []int{15}
}
func (x *GetOIDCURLResponse) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
var File_proxy_service_proto protoreflect.FileDescriptor
var file_proxy_service_proto_rawDesc = []byte{
@@ -1165,130 +1208,147 @@ var file_proxy_service_proto_rawDesc = []byte{
0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x4f, 0x49, 0x44, 0x43, 0x48, 0x00, 0x52, 0x04, 0x6f, 0x69, 0x64, 0x63, 0x88, 0x01, 0x01, 0x12,
0x12, 0x0a, 0x04, 0x6c, 0x69, 0x6e, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x6c,
0x69, 0x6e, 0x6b, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x6f, 0x69, 0x64, 0x63, 0x22, 0x85, 0x01, 0x0a,
0x04, 0x4f, 0x49, 0x44, 0x43, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x1c, 0x0a,
0x09, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09,
0x52, 0x09, 0x61, 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x6b,
0x65, 0x79, 0x73, 0x5f, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x73, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x12, 0x22, 0x0a, 0x0d, 0x6d, 0x61, 0x78, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x61, 0x67,
0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x6d, 0x61, 0x78, 0x54, 0x6f, 0x6b, 0x65,
0x6e, 0x41, 0x67, 0x65, 0x22, 0x87, 0x02, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x61,
0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x36, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64,
0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a,
0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a,
0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28,
0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06,
0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f,
0x6d, 0x61, 0x69, 0x6e, 0x12, 0x2b, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x04, 0x70, 0x61, 0x74,
0x68, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x65, 0x74, 0x75, 0x70, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x06,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12, 0x2e,
0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x6d,
0x69, 0x6e, 0x6b, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x6f, 0x69, 0x64, 0x63, 0x22, 0x37, 0x0a, 0x04,
0x4f, 0x49, 0x44, 0x43, 0x12, 0x2f, 0x0a, 0x13, 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75,
0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
0x09, 0x52, 0x12, 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x47,
0x72, 0x6f, 0x75, 0x70, 0x73, 0x22, 0x87, 0x02, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d,
0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x12, 0x36, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
0x74, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70,
0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0e,
0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d,
0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01,
0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x16, 0x0a,
0x06, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64,
0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x12, 0x2b, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x05, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x2e, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x52, 0x04, 0x70, 0x61,
0x74, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x65, 0x74, 0x75, 0x70, 0x5f, 0x6b, 0x65, 0x79, 0x18,
0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x74, 0x75, 0x70, 0x4b, 0x65, 0x79, 0x12,
0x2e, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65,
0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22,
0x3f, 0x0a, 0x14, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
0x74, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x52, 0x03, 0x6c, 0x6f, 0x67,
0x22, 0x17, 0x0a, 0x15, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f,
0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa0, 0x03, 0x0a, 0x09, 0x41, 0x63,
0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73,
0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
0x70, 0x12, 0x15, 0x0a, 0x06, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x6c, 0x6f, 0x67, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f,
0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69,
0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x05,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61,
0x74, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x1f,
0x0a, 0x0b, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x73, 0x18, 0x07, 0x20,
0x01, 0x28, 0x03, 0x52, 0x0a, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x73, 0x12,
0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52,
0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c,
0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1b, 0x0a, 0x09,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52,
0x08, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x75, 0x74,
0x68, 0x5f, 0x6d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x18, 0x0b, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d,
0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28,
0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x75, 0x74,
0x68, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52,
0x0b, 0x61, 0x75, 0x74, 0x68, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0xe5, 0x01, 0x0a,
0x13, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f,
0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e,
0x74, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18,
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65,
0x6e, 0x74, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x48, 0x00, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x2a,
0x0a, 0x03, 0x70, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x03, 0x70, 0x69, 0x6e, 0x12, 0x2d, 0x0a, 0x04, 0x6c, 0x69,
0x6e, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x69, 0x6e, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x48, 0x00, 0x52, 0x04, 0x6c, 0x69, 0x6e, 0x6b, 0x42, 0x09, 0x0a, 0x07, 0x72, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x22, 0x2d, 0x0a, 0x0f, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77,
0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77,
0x6f, 0x72, 0x64, 0x22, 0x1e, 0x0a, 0x0a, 0x50, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x10, 0x0a, 0x03, 0x70, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
0x70, 0x69, 0x6e, 0x22, 0x3f, 0x0a, 0x0b, 0x4c, 0x69, 0x6e, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x64, 0x69,
0x72, 0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x64, 0x69,
0x72, 0x65, 0x63, 0x74, 0x22, 0x30, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69,
0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07,
0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73,
0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0xfe, 0x01, 0x0a, 0x17, 0x53, 0x65, 0x6e, 0x64, 0x53,
0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x5f, 0x70, 0x72,
0x6f, 0x78, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x72, 0x65,
0x76, 0x65, 0x72, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a,
0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x2f, 0x0a, 0x06, 0x73,
0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x6d, 0x61,
0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x74,
0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x2d, 0x0a, 0x12,
0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x73, 0x73, 0x75,
0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66,
0x69, 0x63, 0x61, 0x74, 0x65, 0x49, 0x73, 0x73, 0x75, 0x65, 0x64, 0x12, 0x28, 0x0a, 0x0d, 0x65,
0x72, 0x72, 0x6f, 0x72, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01,
0x28, 0x09, 0x48, 0x00, 0x52, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x88, 0x01, 0x01, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f,
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x1a, 0x0a, 0x18, 0x53, 0x65, 0x6e, 0x64, 0x53,
0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x2a, 0x64, 0x0a, 0x16, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x61, 0x70, 0x70,
0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a,
0x13, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52, 0x45,
0x41, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45,
0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x01,
0x12, 0x17, 0x0a, 0x13, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f,
0x52, 0x45, 0x4d, 0x4f, 0x56, 0x45, 0x44, 0x10, 0x02, 0x2a, 0xc8, 0x01, 0x0a, 0x0b, 0x50, 0x72,
0x6f, 0x78, 0x79, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x14, 0x50, 0x52, 0x4f,
0x58, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e,
0x47, 0x10, 0x00, 0x12, 0x17, 0x0a, 0x13, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x5f, 0x53, 0x54, 0x41,
0x54, 0x55, 0x53, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x01, 0x12, 0x23, 0x0a, 0x1f,
0x50, 0x52, 0x4f, 0x58, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x54, 0x55, 0x4e,
0x4e, 0x45, 0x4c, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x44, 0x10,
0x02, 0x12, 0x24, 0x0a, 0x20, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55,
0x53, 0x5f, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x45,
0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x03, 0x12, 0x23, 0x0a, 0x1f, 0x50, 0x52, 0x4f, 0x58, 0x59,
0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x43, 0x45, 0x52, 0x54, 0x49, 0x46, 0x49, 0x43,
0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x12, 0x16, 0x0a, 0x12,
0x50, 0x52, 0x4f, 0x58, 0x59, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, 0x52,
0x4f, 0x52, 0x10, 0x05, 0x32, 0xf7, 0x02, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x53, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5f, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x70, 0x70,
0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x23, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e,
0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24,
0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x4d,
0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x54, 0x0a, 0x0d, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63,
0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x12, 0x20, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c,
0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61,
0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73,
0x73, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c,
0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x1f, 0x2e, 0x6d,
0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e,
0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x3f,
0x0a, 0x14, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x27, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x2e, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x22,
0x17, 0x0a, 0x15, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xa0, 0x03, 0x0a, 0x09, 0x41, 0x63, 0x63,
0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
0x12, 0x15, 0x0a, 0x06, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x6c, 0x6f, 0x67, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63,
0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74,
0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x1f, 0x0a,
0x0b, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x73, 0x18, 0x07, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0a, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x73, 0x12, 0x16,
0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x73,
0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x70, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68,
0x5f, 0x6d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0d, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x12,
0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09,
0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x75, 0x74, 0x68,
0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b,
0x61, 0x75, 0x74, 0x68, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0xe5, 0x01, 0x0a, 0x13,
0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69,
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74,
0x49, 0x64, 0x12, 0x39, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
0x74, 0x2e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x48, 0x00, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x2a, 0x0a,
0x03, 0x70, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x6d, 0x61, 0x6e,
0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x48, 0x00, 0x52, 0x03, 0x70, 0x69, 0x6e, 0x12, 0x2d, 0x0a, 0x04, 0x6c, 0x69, 0x6e,
0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65,
0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x69, 0x6e, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x48, 0x00, 0x52, 0x04, 0x6c, 0x69, 0x6e, 0x6b, 0x42, 0x09, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x22, 0x2d, 0x0a, 0x0f, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52,
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f,
0x72, 0x64, 0x22, 0x1e, 0x0a, 0x0a, 0x50, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x12, 0x10, 0x0a, 0x03, 0x70, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x70,
0x69, 0x6e, 0x22, 0x3f, 0x0a, 0x0b, 0x4c, 0x69, 0x6e, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x64, 0x69, 0x72,
0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x64, 0x69, 0x72,
0x65, 0x63, 0x74, 0x22, 0x30, 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63,
0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73,
0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75,
0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x65, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x4f, 0x49, 0x44, 0x43,
0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x65, 0x64,
0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0b, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x72, 0x6c, 0x22, 0x26, 0x0a, 0x12,
0x47, 0x65, 0x74, 0x4f, 0x49, 0x44, 0x43, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x03, 0x75, 0x72, 0x6c, 0x2a, 0x64, 0x0a, 0x16, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x4d, 0x61, 0x70,
0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x17,
0x0a, 0x13, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x52,
0x45, 0x41, 0x54, 0x45, 0x44, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x55, 0x50, 0x44, 0x41, 0x54,
0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4d, 0x4f, 0x44, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10,
0x01, 0x12, 0x17, 0x0a, 0x13, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45,
0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x56, 0x45, 0x44, 0x10, 0x02, 0x32, 0xe5, 0x02, 0x0a, 0x0c, 0x50,
0x72, 0x6f, 0x78, 0x79, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5f, 0x0a, 0x10, 0x47,
0x65, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12,
0x23, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x74,
0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e,
0x74, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x55, 0x70, 0x64, 0x61,
0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x54, 0x0a, 0x0d,
0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x12, 0x20, 0x2e,
0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x41,
0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x21, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x6e,
0x64, 0x41, 0x63, 0x63, 0x65, 0x73, 0x73, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61,
0x74, 0x65, 0x12, 0x1f, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73,
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x4f, 0x49, 0x44, 0x43,
0x55, 0x52, 0x4c, 0x12, 0x1d, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x2e, 0x47, 0x65, 0x74, 0x4f, 0x49, 0x44, 0x43, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e,
0x47, 0x65, 0x74, 0x4f, 0x49, 0x44, 0x43, 0x55, 0x52, 0x4c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x42, 0x08, 0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e,
0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65,
0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x5d, 0x0a, 0x10, 0x53, 0x65, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64,
0x61, 0x74, 0x65, 0x12, 0x23, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74,
0x2e, 0x53, 0x65, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x55, 0x70, 0x64, 0x61, 0x74,
0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x65, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73,
0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x08,
0x5a, 0x06, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -1303,53 +1363,55 @@ func file_proxy_service_proto_rawDescGZIP() []byte {
return file_proxy_service_proto_rawDescData
}
var file_proxy_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_proxy_service_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_proxy_service_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
var file_proxy_service_proto_goTypes = []interface{}{
(ProxyMappingUpdateType)(0), // 0: management.ProxyMappingUpdateType
(*GetMappingUpdateRequest)(nil), // 1: management.GetMappingUpdateRequest
(*GetMappingUpdateResponse)(nil), // 2: management.GetMappingUpdateResponse
(*PathMapping)(nil), // 3: management.PathMapping
(*Authentication)(nil), // 4: management.Authentication
(*OIDC)(nil), // 5: management.OIDC
(*ProxyMapping)(nil), // 6: management.ProxyMapping
(*SendAccessLogRequest)(nil), // 7: management.SendAccessLogRequest
(*SendAccessLogResponse)(nil), // 8: management.SendAccessLogResponse
(*AccessLog)(nil), // 9: management.AccessLog
(*AuthenticateRequest)(nil), // 10: management.AuthenticateRequest
(*PasswordRequest)(nil), // 11: management.PasswordRequest
(*PinRequest)(nil), // 12: management.PinRequest
(*LinkRequest)(nil), // 13: management.LinkRequest
(*AuthenticateResponse)(nil), // 14: management.AuthenticateResponse
(*GetOIDCURLRequest)(nil), // 15: management.GetOIDCURLRequest
(*GetOIDCURLResponse)(nil), // 16: management.GetOIDCURLResponse
(*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp
(ProxyStatus)(0), // 1: management.ProxyStatus
(*GetMappingUpdateRequest)(nil), // 2: management.GetMappingUpdateRequest
(*GetMappingUpdateResponse)(nil), // 3: management.GetMappingUpdateResponse
(*PathMapping)(nil), // 4: management.PathMapping
(*Authentication)(nil), // 5: management.Authentication
(*OIDC)(nil), // 6: management.OIDC
(*ProxyMapping)(nil), // 7: management.ProxyMapping
(*SendAccessLogRequest)(nil), // 8: management.SendAccessLogRequest
(*SendAccessLogResponse)(nil), // 9: management.SendAccessLogResponse
(*AccessLog)(nil), // 10: management.AccessLog
(*AuthenticateRequest)(nil), // 11: management.AuthenticateRequest
(*PasswordRequest)(nil), // 12: management.PasswordRequest
(*PinRequest)(nil), // 13: management.PinRequest
(*LinkRequest)(nil), // 14: management.LinkRequest
(*AuthenticateResponse)(nil), // 15: management.AuthenticateResponse
(*SendStatusUpdateRequest)(nil), // 16: management.SendStatusUpdateRequest
(*SendStatusUpdateResponse)(nil), // 17: management.SendStatusUpdateResponse
(*timestamppb.Timestamp)(nil), // 18: google.protobuf.Timestamp
}
var file_proxy_service_proto_depIdxs = []int32{
17, // 0: management.GetMappingUpdateRequest.started_at:type_name -> google.protobuf.Timestamp
6, // 1: management.GetMappingUpdateResponse.mapping:type_name -> management.ProxyMapping
5, // 2: management.Authentication.oidc:type_name -> management.OIDC
18, // 0: management.GetMappingUpdateRequest.started_at:type_name -> google.protobuf.Timestamp
7, // 1: management.GetMappingUpdateResponse.mapping:type_name -> management.ProxyMapping
6, // 2: management.Authentication.oidc:type_name -> management.OIDC
0, // 3: management.ProxyMapping.type:type_name -> management.ProxyMappingUpdateType
3, // 4: management.ProxyMapping.path:type_name -> management.PathMapping
4, // 5: management.ProxyMapping.auth:type_name -> management.Authentication
9, // 6: management.SendAccessLogRequest.log:type_name -> management.AccessLog
17, // 7: management.AccessLog.timestamp:type_name -> google.protobuf.Timestamp
11, // 8: management.AuthenticateRequest.password:type_name -> management.PasswordRequest
12, // 9: management.AuthenticateRequest.pin:type_name -> management.PinRequest
13, // 10: management.AuthenticateRequest.link:type_name -> management.LinkRequest
1, // 11: management.ProxyService.GetMappingUpdate:input_type -> management.GetMappingUpdateRequest
7, // 12: management.ProxyService.SendAccessLog:input_type -> management.SendAccessLogRequest
10, // 13: management.ProxyService.Authenticate:input_type -> management.AuthenticateRequest
15, // 14: management.ProxyService.GetOIDCURL:input_type -> management.GetOIDCURLRequest
2, // 15: management.ProxyService.GetMappingUpdate:output_type -> management.GetMappingUpdateResponse
8, // 16: management.ProxyService.SendAccessLog:output_type -> management.SendAccessLogResponse
14, // 17: management.ProxyService.Authenticate:output_type -> management.AuthenticateResponse
16, // 18: management.ProxyService.GetOIDCURL:output_type -> management.GetOIDCURLResponse
15, // [15:19] is the sub-list for method output_type
11, // [11:15] is the sub-list for method input_type
11, // [11:11] is the sub-list for extension type_name
11, // [11:11] is the sub-list for extension extendee
0, // [0:11] is the sub-list for field type_name
4, // 4: management.ProxyMapping.path:type_name -> management.PathMapping
5, // 5: management.ProxyMapping.auth:type_name -> management.Authentication
10, // 6: management.SendAccessLogRequest.log:type_name -> management.AccessLog
18, // 7: management.AccessLog.timestamp:type_name -> google.protobuf.Timestamp
12, // 8: management.AuthenticateRequest.password:type_name -> management.PasswordRequest
13, // 9: management.AuthenticateRequest.pin:type_name -> management.PinRequest
14, // 10: management.AuthenticateRequest.link:type_name -> management.LinkRequest
1, // 11: management.SendStatusUpdateRequest.status:type_name -> management.ProxyStatus
2, // 12: management.ProxyService.GetMappingUpdate:input_type -> management.GetMappingUpdateRequest
8, // 13: management.ProxyService.SendAccessLog:input_type -> management.SendAccessLogRequest
11, // 14: management.ProxyService.Authenticate:input_type -> management.AuthenticateRequest
16, // 15: management.ProxyService.SendStatusUpdate:input_type -> management.SendStatusUpdateRequest
3, // 16: management.ProxyService.GetMappingUpdate:output_type -> management.GetMappingUpdateResponse
9, // 17: management.ProxyService.SendAccessLog:output_type -> management.SendAccessLogResponse
15, // 18: management.ProxyService.Authenticate:output_type -> management.AuthenticateResponse
17, // 19: management.ProxyService.SendStatusUpdate:output_type -> management.SendStatusUpdateResponse
16, // [16:20] is the sub-list for method output_type
12, // [12:16] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
}
func init() { file_proxy_service_proto_init() }
@@ -1527,7 +1589,7 @@ func file_proxy_service_proto_init() {
}
}
file_proxy_service_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetOIDCURLRequest); i {
switch v := v.(*SendStatusUpdateRequest); i {
case 0:
return &v.state
case 1:
@@ -1539,7 +1601,7 @@ func file_proxy_service_proto_init() {
}
}
file_proxy_service_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*GetOIDCURLResponse); i {
switch v := v.(*SendStatusUpdateResponse); i {
case 0:
return &v.state
case 1:
@@ -1557,12 +1619,13 @@ func file_proxy_service_proto_init() {
(*AuthenticateRequest_Pin)(nil),
(*AuthenticateRequest_Link)(nil),
}
file_proxy_service_proto_msgTypes[14].OneofWrappers = []interface{}{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proxy_service_proto_rawDesc,
NumEnums: 1,
NumEnums: 2,
NumMessages: 16,
NumExtensions: 0,
NumServices: 1,

View File

@@ -15,6 +15,8 @@ service ProxyService {
rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse);
rpc SendStatusUpdate(SendStatusUpdateRequest) returns (SendStatusUpdateResponse);
rpc GetOIDCURL(GetOIDCURLRequest) returns (GetOIDCURLResponse);
}
@@ -119,6 +121,27 @@ message AuthenticateResponse {
bool success = 1;
}
enum ProxyStatus {
PROXY_STATUS_PENDING = 0;
PROXY_STATUS_ACTIVE = 1;
PROXY_STATUS_TUNNEL_NOT_CREATED = 2;
PROXY_STATUS_CERTIFICATE_PENDING = 3;
PROXY_STATUS_CERTIFICATE_FAILED = 4;
PROXY_STATUS_ERROR = 5;
}
// SendStatusUpdateRequest is sent by the proxy to update its status
message SendStatusUpdateRequest {
string reverse_proxy_id = 1;
string account_id = 2;
ProxyStatus status = 3;
bool certificate_issued = 4;
optional string error_message = 5;
}
// SendStatusUpdateResponse is intentionally empty to allow for future expansion
message SendStatusUpdateResponse {}
message GetOIDCURLRequest {
string id = 1;
string account_id = 2;

View File

@@ -21,7 +21,7 @@ type ProxyServiceClient interface {
GetMappingUpdate(ctx context.Context, in *GetMappingUpdateRequest, opts ...grpc.CallOption) (ProxyService_GetMappingUpdateClient, error)
SendAccessLog(ctx context.Context, in *SendAccessLogRequest, opts ...grpc.CallOption) (*SendAccessLogResponse, error)
Authenticate(ctx context.Context, in *AuthenticateRequest, opts ...grpc.CallOption) (*AuthenticateResponse, error)
GetOIDCURL(ctx context.Context, in *GetOIDCURLRequest, opts ...grpc.CallOption) (*GetOIDCURLResponse, error)
SendStatusUpdate(ctx context.Context, in *SendStatusUpdateRequest, opts ...grpc.CallOption) (*SendStatusUpdateResponse, error)
}
type proxyServiceClient struct {
@@ -82,9 +82,9 @@ func (c *proxyServiceClient) Authenticate(ctx context.Context, in *AuthenticateR
return out, nil
}
func (c *proxyServiceClient) GetOIDCURL(ctx context.Context, in *GetOIDCURLRequest, opts ...grpc.CallOption) (*GetOIDCURLResponse, error) {
out := new(GetOIDCURLResponse)
err := c.cc.Invoke(ctx, "/management.ProxyService/GetOIDCURL", in, out, opts...)
func (c *proxyServiceClient) SendStatusUpdate(ctx context.Context, in *SendStatusUpdateRequest, opts ...grpc.CallOption) (*SendStatusUpdateResponse, error) {
out := new(SendStatusUpdateResponse)
err := c.cc.Invoke(ctx, "/management.ProxyService/SendStatusUpdate", in, out, opts...)
if err != nil {
return nil, err
}
@@ -98,7 +98,7 @@ type ProxyServiceServer interface {
GetMappingUpdate(*GetMappingUpdateRequest, ProxyService_GetMappingUpdateServer) error
SendAccessLog(context.Context, *SendAccessLogRequest) (*SendAccessLogResponse, error)
Authenticate(context.Context, *AuthenticateRequest) (*AuthenticateResponse, error)
GetOIDCURL(context.Context, *GetOIDCURLRequest) (*GetOIDCURLResponse, error)
SendStatusUpdate(context.Context, *SendStatusUpdateRequest) (*SendStatusUpdateResponse, error)
mustEmbedUnimplementedProxyServiceServer()
}
@@ -115,8 +115,8 @@ func (UnimplementedProxyServiceServer) SendAccessLog(context.Context, *SendAcces
func (UnimplementedProxyServiceServer) Authenticate(context.Context, *AuthenticateRequest) (*AuthenticateResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented")
}
func (UnimplementedProxyServiceServer) GetOIDCURL(context.Context, *GetOIDCURLRequest) (*GetOIDCURLResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetOIDCURL not implemented")
func (UnimplementedProxyServiceServer) SendStatusUpdate(context.Context, *SendStatusUpdateRequest) (*SendStatusUpdateResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SendStatusUpdate not implemented")
}
func (UnimplementedProxyServiceServer) mustEmbedUnimplementedProxyServiceServer() {}
@@ -188,20 +188,20 @@ func _ProxyService_Authenticate_Handler(srv interface{}, ctx context.Context, de
return interceptor(ctx, in, info, handler)
}
func _ProxyService_GetOIDCURL_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetOIDCURLRequest)
func _ProxyService_SendStatusUpdate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SendStatusUpdateRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ProxyServiceServer).GetOIDCURL(ctx, in)
return srv.(ProxyServiceServer).SendStatusUpdate(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/management.ProxyService/GetOIDCURL",
FullMethod: "/management.ProxyService/SendStatusUpdate",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ProxyServiceServer).GetOIDCURL(ctx, req.(*GetOIDCURLRequest))
return srv.(ProxyServiceServer).SendStatusUpdate(ctx, req.(*SendStatusUpdateRequest))
}
return interceptor(ctx, in, info, handler)
}
@@ -222,8 +222,8 @@ var ProxyService_ServiceDesc = grpc.ServiceDesc{
Handler: _ProxyService_Authenticate_Handler,
},
{
MethodName: "GetOIDCURL",
Handler: _ProxyService_GetOIDCURL_Handler,
MethodName: "SendStatusUpdate",
Handler: _ProxyService_SendStatusUpdate_Handler,
},
},
Streams: []grpc.StreamDesc{