mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 08:16:39 +00:00
Merge branch 'main' into refactor/permissions-manager
# Conflicts: # management/server/account_test.go # management/server/http/handlers/networks/routers_handler.go
This commit is contained in:
62
.github/workflows/proto-version-check.yml
vendored
Normal file
62
.github/workflows/proto-version-check.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
name: Proto Version Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "**/*.pb.go"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-proto-versions:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check for proto tool version changes
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: context.issue.number,
|
||||||
|
per_page: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pbFiles = files.filter(f => f.filename.endsWith('.pb.go'));
|
||||||
|
const missingPatch = pbFiles.filter(f => !f.patch).map(f => f.filename);
|
||||||
|
if (missingPatch.length > 0) {
|
||||||
|
core.setFailed(
|
||||||
|
`Cannot inspect patch data for:\n` +
|
||||||
|
missingPatch.map(f => `- ${f}`).join('\n') +
|
||||||
|
`\nThis can happen with very large PRs. Verify proto versions manually.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const versionPattern = /^[+-]\s*\/\/\s+protoc(?:-gen-go)?\s+v[\d.]+/;
|
||||||
|
const violations = [];
|
||||||
|
|
||||||
|
for (const file of pbFiles) {
|
||||||
|
const changed = file.patch
|
||||||
|
.split('\n')
|
||||||
|
.filter(line => versionPattern.test(line));
|
||||||
|
if (changed.length > 0) {
|
||||||
|
violations.push({
|
||||||
|
file: file.filename,
|
||||||
|
lines: changed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (violations.length > 0) {
|
||||||
|
const details = violations.map(v =>
|
||||||
|
`${v.file}:\n${v.lines.map(l => ' ' + l).join('\n')}`
|
||||||
|
).join('\n\n');
|
||||||
|
|
||||||
|
core.setFailed(
|
||||||
|
`Proto version strings changed in generated files.\n` +
|
||||||
|
`This usually means the wrong protoc or protoc-gen-go version was used.\n` +
|
||||||
|
`Regenerate with the matching tool versions.\n\n` +
|
||||||
|
details
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('No proto version string changes detected');
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -9,7 +9,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.1.1"
|
SIGN_PIPE_VER: "v0.1.2"
|
||||||
GORELEASER_VER: "v2.14.3"
|
GORELEASER_VER: "v2.14.3"
|
||||||
PRODUCT_NAME: "NetBird"
|
PRODUCT_NAME: "NetBird"
|
||||||
COPYRIGHT: "NetBird GmbH"
|
COPYRIGHT: "NetBird GmbH"
|
||||||
|
|||||||
@@ -199,9 +199,11 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
|||||||
cmd.Println("Log level set to trace.")
|
cmd.Println("Log level set to trace.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
needsRestoreUp := false
|
||||||
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
|
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
|
||||||
cmd.PrintErrf("Failed to bring service down: %v\n", status.Convert(err).Message())
|
cmd.PrintErrf("Failed to bring service down: %v\n", status.Convert(err).Message())
|
||||||
} else {
|
} else {
|
||||||
|
needsRestoreUp = !stateWasDown
|
||||||
cmd.Println("netbird down")
|
cmd.Println("netbird down")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,6 +219,7 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
|||||||
if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil {
|
if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil {
|
||||||
cmd.PrintErrf("Failed to bring service up: %v\n", status.Convert(err).Message())
|
cmd.PrintErrf("Failed to bring service up: %v\n", status.Convert(err).Message())
|
||||||
} else {
|
} else {
|
||||||
|
needsRestoreUp = false
|
||||||
cmd.Println("netbird up")
|
cmd.Println("netbird up")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,6 +267,14 @@ func runForDuration(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
|
return fmt.Errorf("failed to bundle debug: %v", status.Convert(err).Message())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if needsRestoreUp {
|
||||||
|
if _, err := client.Up(cmd.Context(), &proto.UpRequest{}); err != nil {
|
||||||
|
cmd.PrintErrf("Failed to restore service up state: %v\n", status.Convert(err).Message())
|
||||||
|
} else {
|
||||||
|
cmd.Println("netbird up (restored)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if stateWasDown {
|
if stateWasDown {
|
||||||
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
|
if _, err := client.Down(cmd.Context(), &proto.DownRequest{}); err != nil {
|
||||||
cmd.PrintErrf("Failed to restore service down state: %v\n", status.Convert(err).Message())
|
cmd.PrintErrf("Failed to restore service down state: %v\n", status.Convert(err).Message())
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/expose"
|
"github.com/netbirdio/netbird/client/internal/expose"
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
@@ -201,7 +202,7 @@ func exposeFn(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
stream, err := client.ExposeService(ctx, req)
|
stream, err := client.ExposeService(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("expose service: %w", err)
|
return fmt.Errorf("expose service: %v", status.Convert(err).Message())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := handleExposeReady(cmd, stream, port); err != nil {
|
if err := handleExposeReady(cmd, stream, port); err != nil {
|
||||||
@@ -236,7 +237,7 @@ func toExposeProtocol(exposeProtocol string) (proto.ExposeProtocol, error) {
|
|||||||
func handleExposeReady(cmd *cobra.Command, stream proto.DaemonService_ExposeServiceClient, port uint64) error {
|
func handleExposeReady(cmd *cobra.Command, stream proto.DaemonService_ExposeServiceClient, port uint64) error {
|
||||||
event, err := stream.Recv()
|
event, err := stream.Recv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("receive expose event: %w", err)
|
return fmt.Errorf("receive expose event: %v", status.Convert(err).Message())
|
||||||
}
|
}
|
||||||
|
|
||||||
ready, ok := event.Event.(*proto.ExposeServiceEvent_Ready)
|
ready, ok := event.Event.(*proto.ExposeServiceEvent_Ready)
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ var (
|
|||||||
mtu uint16
|
mtu uint16
|
||||||
profilesDisabled bool
|
profilesDisabled bool
|
||||||
updateSettingsDisabled bool
|
updateSettingsDisabled bool
|
||||||
|
networksDisabled bool
|
||||||
|
|
||||||
rootCmd = &cobra.Command{
|
rootCmd = &cobra.Command{
|
||||||
Use: "netbird",
|
Use: "netbird",
|
||||||
|
|||||||
@@ -44,10 +44,13 @@ func init() {
|
|||||||
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd, resetParamsCmd)
|
serviceCmd.AddCommand(runCmd, startCmd, stopCmd, restartCmd, svcStatusCmd, installCmd, uninstallCmd, reconfigureCmd, resetParamsCmd)
|
||||||
serviceCmd.PersistentFlags().BoolVar(&profilesDisabled, "disable-profiles", false, "Disables profiles feature. If enabled, the client will not be able to change or edit any profile. To persist this setting, use: netbird service install --disable-profiles")
|
serviceCmd.PersistentFlags().BoolVar(&profilesDisabled, "disable-profiles", false, "Disables profiles feature. If enabled, the client will not be able to change or edit any profile. To persist this setting, use: netbird service install --disable-profiles")
|
||||||
serviceCmd.PersistentFlags().BoolVar(&updateSettingsDisabled, "disable-update-settings", false, "Disables update settings feature. If enabled, the client will not be able to change or edit any settings. To persist this setting, use: netbird service install --disable-update-settings")
|
serviceCmd.PersistentFlags().BoolVar(&updateSettingsDisabled, "disable-update-settings", false, "Disables update settings feature. If enabled, the client will not be able to change or edit any settings. To persist this setting, use: netbird service install --disable-update-settings")
|
||||||
|
serviceCmd.PersistentFlags().BoolVar(&networksDisabled, "disable-networks", false, "Disables network selection. If enabled, the client will not allow listing, selecting, or deselecting networks. To persist, use: netbird service install --disable-networks")
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
|
rootCmd.PersistentFlags().StringVarP(&serviceName, "service", "s", defaultServiceName, "Netbird system service name")
|
||||||
serviceEnvDesc := `Sets extra environment variables for the service. ` +
|
serviceEnvDesc := `Sets extra environment variables for the service. ` +
|
||||||
`You can specify a comma-separated list of KEY=VALUE pairs. ` +
|
`You can specify a comma-separated list of KEY=VALUE pairs. ` +
|
||||||
|
`New keys are merged with previously saved env vars; existing keys are overwritten. ` +
|
||||||
|
`Use --service-env "" to clear all saved env vars. ` +
|
||||||
`E.g. --service-env NB_LOG_LEVEL=debug,CUSTOM_VAR=value`
|
`E.g. --service-env NB_LOG_LEVEL=debug,CUSTOM_VAR=value`
|
||||||
|
|
||||||
installCmd.Flags().StringSliceVar(&serviceEnvVars, "service-env", nil, serviceEnvDesc)
|
installCmd.Flags().StringSliceVar(&serviceEnvVars, "service-env", nil, serviceEnvDesc)
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func (p *program) Start(svc service.Service) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled)
|
serverInstance := server.New(p.ctx, util.FindFirstLogPath(logFiles), configPath, profilesDisabled, updateSettingsDisabled, networksDisabled)
|
||||||
if err := serverInstance.Start(); err != nil {
|
if err := serverInstance.Start(); err != nil {
|
||||||
log.Fatalf("failed to start daemon: %v", err)
|
log.Fatalf("failed to start daemon: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ func buildServiceArguments() []string {
|
|||||||
args = append(args, "--disable-update-settings")
|
args = append(args, "--disable-update-settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if networksDisabled {
|
||||||
|
args = append(args, "--disable-networks")
|
||||||
|
}
|
||||||
|
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type serviceParams struct {
|
|||||||
LogFiles []string `json:"log_files,omitempty"`
|
LogFiles []string `json:"log_files,omitempty"`
|
||||||
DisableProfiles bool `json:"disable_profiles,omitempty"`
|
DisableProfiles bool `json:"disable_profiles,omitempty"`
|
||||||
DisableUpdateSettings bool `json:"disable_update_settings,omitempty"`
|
DisableUpdateSettings bool `json:"disable_update_settings,omitempty"`
|
||||||
|
DisableNetworks bool `json:"disable_networks,omitempty"`
|
||||||
ServiceEnvVars map[string]string `json:"service_env_vars,omitempty"`
|
ServiceEnvVars map[string]string `json:"service_env_vars,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,11 +79,12 @@ func currentServiceParams() *serviceParams {
|
|||||||
LogFiles: logFiles,
|
LogFiles: logFiles,
|
||||||
DisableProfiles: profilesDisabled,
|
DisableProfiles: profilesDisabled,
|
||||||
DisableUpdateSettings: updateSettingsDisabled,
|
DisableUpdateSettings: updateSettingsDisabled,
|
||||||
|
DisableNetworks: networksDisabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(serviceEnvVars) > 0 {
|
if len(serviceEnvVars) > 0 {
|
||||||
parsed, err := parseServiceEnvVars(serviceEnvVars)
|
parsed, err := parseServiceEnvVars(serviceEnvVars)
|
||||||
if err == nil && len(parsed) > 0 {
|
if err == nil {
|
||||||
params.ServiceEnvVars = parsed
|
params.ServiceEnvVars = parsed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,31 +144,46 @@ func applyServiceParams(cmd *cobra.Command, params *serviceParams) {
|
|||||||
updateSettingsDisabled = params.DisableUpdateSettings
|
updateSettingsDisabled = params.DisableUpdateSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !serviceCmd.PersistentFlags().Changed("disable-networks") {
|
||||||
|
networksDisabled = params.DisableNetworks
|
||||||
|
}
|
||||||
|
|
||||||
applyServiceEnvParams(cmd, params)
|
applyServiceEnvParams(cmd, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyServiceEnvParams merges saved service environment variables.
|
// applyServiceEnvParams merges saved service environment variables.
|
||||||
// If --service-env was explicitly set, explicit values win on key conflict
|
// If --service-env was explicitly set with values, explicit values win on key
|
||||||
// but saved keys not in the explicit set are carried over.
|
// conflict but saved keys not in the explicit set are carried over.
|
||||||
|
// If --service-env was explicitly set to empty, all saved env vars are cleared.
|
||||||
// If --service-env was not set, saved env vars are used entirely.
|
// If --service-env was not set, saved env vars are used entirely.
|
||||||
func applyServiceEnvParams(cmd *cobra.Command, params *serviceParams) {
|
func applyServiceEnvParams(cmd *cobra.Command, params *serviceParams) {
|
||||||
if len(params.ServiceEnvVars) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cmd.Flags().Changed("service-env") {
|
if !cmd.Flags().Changed("service-env") {
|
||||||
// No explicit env vars: rebuild serviceEnvVars from saved params.
|
if len(params.ServiceEnvVars) > 0 {
|
||||||
serviceEnvVars = envMapToSlice(params.ServiceEnvVars)
|
// No explicit env vars: rebuild serviceEnvVars from saved params.
|
||||||
|
serviceEnvVars = envMapToSlice(params.ServiceEnvVars)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicit env vars were provided: merge saved values underneath.
|
// Flag was explicitly set: parse what the user provided.
|
||||||
explicit, err := parseServiceEnvVars(serviceEnvVars)
|
explicit, err := parseServiceEnvVars(serviceEnvVars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cmd.PrintErrf("Warning: parse explicit service env vars for merge: %v\n", err)
|
cmd.PrintErrf("Warning: parse explicit service env vars for merge: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the user passed an empty value (e.g. --service-env ""), clear all
|
||||||
|
// saved env vars rather than merging.
|
||||||
|
if len(explicit) == 0 {
|
||||||
|
serviceEnvVars = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params.ServiceEnvVars) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge saved values underneath explicit ones.
|
||||||
merged := make(map[string]string, len(params.ServiceEnvVars)+len(explicit))
|
merged := make(map[string]string, len(params.ServiceEnvVars)+len(explicit))
|
||||||
maps.Copy(merged, params.ServiceEnvVars)
|
maps.Copy(merged, params.ServiceEnvVars)
|
||||||
maps.Copy(merged, explicit) // explicit wins on conflict
|
maps.Copy(merged, explicit) // explicit wins on conflict
|
||||||
|
|||||||
@@ -327,6 +327,41 @@ func TestApplyServiceEnvParams_NotChanged(t *testing.T) {
|
|||||||
assert.Equal(t, map[string]string{"FROM_SAVED": "val"}, result)
|
assert.Equal(t, map[string]string{"FROM_SAVED": "val"}, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyServiceEnvParams_ExplicitEmptyClears(t *testing.T) {
|
||||||
|
origServiceEnvVars := serviceEnvVars
|
||||||
|
t.Cleanup(func() { serviceEnvVars = origServiceEnvVars })
|
||||||
|
|
||||||
|
// Simulate --service-env "" which produces [""] in the slice.
|
||||||
|
serviceEnvVars = []string{""}
|
||||||
|
|
||||||
|
cmd := &cobra.Command{}
|
||||||
|
cmd.Flags().StringSlice("service-env", nil, "")
|
||||||
|
require.NoError(t, cmd.Flags().Set("service-env", ""))
|
||||||
|
|
||||||
|
saved := &serviceParams{
|
||||||
|
ServiceEnvVars: map[string]string{"OLD_VAR": "should_be_cleared"},
|
||||||
|
}
|
||||||
|
|
||||||
|
applyServiceEnvParams(cmd, saved)
|
||||||
|
|
||||||
|
assert.Nil(t, serviceEnvVars, "explicit empty --service-env should clear all saved env vars")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCurrentServiceParams_EmptyEnvVarsAfterParse(t *testing.T) {
|
||||||
|
origServiceEnvVars := serviceEnvVars
|
||||||
|
t.Cleanup(func() { serviceEnvVars = origServiceEnvVars })
|
||||||
|
|
||||||
|
// Simulate --service-env "" which produces [""] in the slice.
|
||||||
|
serviceEnvVars = []string{""}
|
||||||
|
|
||||||
|
params := currentServiceParams()
|
||||||
|
|
||||||
|
// After parsing, the empty string is skipped, resulting in an empty map.
|
||||||
|
// The map should still be set (not nil) so it overwrites saved values.
|
||||||
|
assert.NotNil(t, params.ServiceEnvVars, "empty env vars should produce empty map, not nil")
|
||||||
|
assert.Empty(t, params.ServiceEnvVars, "no valid env vars should be parsed from empty string")
|
||||||
|
}
|
||||||
|
|
||||||
// TestServiceParams_FieldsCoveredInFunctions ensures that all serviceParams fields are
|
// TestServiceParams_FieldsCoveredInFunctions ensures that all serviceParams fields are
|
||||||
// referenced in both currentServiceParams() and applyServiceParams(). If a new field is
|
// referenced in both currentServiceParams() and applyServiceParams(). If a new field is
|
||||||
// added to serviceParams but not wired into these functions, this test fails.
|
// added to serviceParams but not wired into these functions, this test fails.
|
||||||
@@ -500,6 +535,7 @@ func fieldToGlobalVar(field string) string {
|
|||||||
"LogFiles": "logFiles",
|
"LogFiles": "logFiles",
|
||||||
"DisableProfiles": "profilesDisabled",
|
"DisableProfiles": "profilesDisabled",
|
||||||
"DisableUpdateSettings": "updateSettingsDisabled",
|
"DisableUpdateSettings": "updateSettingsDisabled",
|
||||||
|
"DisableNetworks": "networksDisabled",
|
||||||
"ServiceEnvVars": "serviceEnvVars",
|
"ServiceEnvVars": "serviceEnvVars",
|
||||||
}
|
}
|
||||||
if v, ok := m[field]; ok {
|
if v, ok := m[field]; ok {
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ func startClientDaemon(
|
|||||||
s := grpc.NewServer()
|
s := grpc.NewServer()
|
||||||
|
|
||||||
server := client.New(ctx,
|
server := client.New(ctx,
|
||||||
"", "", false, false)
|
"", "", false, false, false)
|
||||||
if err := server.Start(); err != nil {
|
if err := server.Start(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/coreos/go-iptables/iptables"
|
"github.com/coreos/go-iptables/iptables"
|
||||||
"github.com/google/nftables"
|
"github.com/google/nftables"
|
||||||
@@ -35,20 +36,34 @@ const SKIP_NFTABLES_ENV = "NB_SKIP_NFTABLES_CHECK"
|
|||||||
type FWType int
|
type FWType int
|
||||||
|
|
||||||
func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager, flowLogger nftypes.FlowLogger, disableServerRoutes bool, mtu uint16) (firewall.Manager, error) {
|
func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager, flowLogger nftypes.FlowLogger, disableServerRoutes bool, mtu uint16) (firewall.Manager, error) {
|
||||||
// on the linux system we try to user nftables or iptables
|
// We run in userspace mode and force userspace firewall was requested. We don't attempt native firewall.
|
||||||
// in any case, because we need to allow netbird interface traffic
|
if iface.IsUserspaceBind() && forceUserspaceFirewall() {
|
||||||
// so we use AllowNetbird traffic from these firewall managers
|
log.Info("forcing userspace firewall")
|
||||||
// for the userspace packet filtering firewall
|
return createUserspaceFirewall(iface, nil, disableServerRoutes, flowLogger, mtu)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use native firewall for either kernel or userspace, the interface appears identical to netfilter
|
||||||
fm, err := createNativeFirewall(iface, stateManager, disableServerRoutes, mtu)
|
fm, err := createNativeFirewall(iface, stateManager, disableServerRoutes, mtu)
|
||||||
|
|
||||||
|
// Kernel cannot fall back to anything else, need to return error
|
||||||
if !iface.IsUserspaceBind() {
|
if !iface.IsUserspaceBind() {
|
||||||
return fm, err
|
return fm, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fall back to the userspace packet filter if native is unavailable
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to create native firewall: %v. Proceeding with userspace", err)
|
log.Warnf("failed to create native firewall: %v. Proceeding with userspace", err)
|
||||||
|
return createUserspaceFirewall(iface, nil, disableServerRoutes, flowLogger, mtu)
|
||||||
}
|
}
|
||||||
return createUserspaceFirewall(iface, fm, disableServerRoutes, flowLogger, mtu)
|
|
||||||
|
// Native firewall handles packet filtering, but the userspace WireGuard bind
|
||||||
|
// needs a device filter for DNS interception hooks. Install a minimal
|
||||||
|
// hooks-only filter that passes all traffic through to the kernel firewall.
|
||||||
|
if err := iface.SetFilter(&uspfilter.HooksFilter{}); err != nil {
|
||||||
|
log.Warnf("failed to set hooks filter, DNS via memory hooks will not work: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createNativeFirewall(iface IFaceMapper, stateManager *statemanager.Manager, routes bool, mtu uint16) (firewall.Manager, error) {
|
func createNativeFirewall(iface IFaceMapper, stateManager *statemanager.Manager, routes bool, mtu uint16) (firewall.Manager, error) {
|
||||||
@@ -160,3 +175,17 @@ func isIptablesClientAvailable(client *iptables.IPTables) bool {
|
|||||||
_, err := client.ListChains("filter")
|
_, err := client.ListChains("filter")
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func forceUserspaceFirewall() bool {
|
||||||
|
val := os.Getenv(EnvForceUserspaceFirewall)
|
||||||
|
if val == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
force, err := strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to parse %s: %v", EnvForceUserspaceFirewall, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return force
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// EnvForceUserspaceFirewall forces the use of the userspace packet filter even when
|
||||||
|
// native iptables/nftables is available. This only applies when the WireGuard interface
|
||||||
|
// runs in userspace mode. When set, peer ACLs are handled by USPFilter instead of
|
||||||
|
// kernel netfilter rules.
|
||||||
|
const EnvForceUserspaceFirewall = "NB_FORCE_USERSPACE_FIREWALL"
|
||||||
|
|
||||||
// IFaceMapper defines subset methods of interface required for manager
|
// IFaceMapper defines subset methods of interface required for manager
|
||||||
type IFaceMapper interface {
|
type IFaceMapper interface {
|
||||||
Name() string
|
Name() string
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ const (
|
|||||||
|
|
||||||
// rules chains contains the effective ACL rules
|
// rules chains contains the effective ACL rules
|
||||||
chainNameInputRules = "NETBIRD-ACL-INPUT"
|
chainNameInputRules = "NETBIRD-ACL-INPUT"
|
||||||
|
|
||||||
|
// mangleFwdKey is the entries map key for mangle FORWARD guard rules that prevent
|
||||||
|
// external DNAT from bypassing ACL rules.
|
||||||
|
mangleFwdKey = "MANGLE-FORWARD"
|
||||||
)
|
)
|
||||||
|
|
||||||
type aclEntries map[string][][]string
|
type aclEntries map[string][][]string
|
||||||
@@ -274,6 +278,12 @@ func (m *aclManager) cleanChains() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, rule := range m.entries[mangleFwdKey] {
|
||||||
|
if err := m.iptablesClient.DeleteIfExists(tableMangle, chainFORWARD, rule...); err != nil {
|
||||||
|
log.Errorf("failed to delete mangle FORWARD guard rule: %v, %s", rule, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, ipsetName := range m.ipsetStore.ipsetNames() {
|
for _, ipsetName := range m.ipsetStore.ipsetNames() {
|
||||||
if err := m.flushIPSet(ipsetName); err != nil {
|
if err := m.flushIPSet(ipsetName); err != nil {
|
||||||
if errors.Is(err, ipset.ErrSetNotExist) {
|
if errors.Is(err, ipset.ErrSetNotExist) {
|
||||||
@@ -303,6 +313,10 @@ func (m *aclManager) createDefaultChains() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for chainName, rules := range m.entries {
|
for chainName, rules := range m.entries {
|
||||||
|
// mangle FORWARD guard rules are handled separately below
|
||||||
|
if chainName == mangleFwdKey {
|
||||||
|
continue
|
||||||
|
}
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
if err := m.iptablesClient.InsertUnique(tableName, chainName, 1, rule...); err != nil {
|
if err := m.iptablesClient.InsertUnique(tableName, chainName, 1, rule...); err != nil {
|
||||||
log.Debugf("failed to create input chain jump rule: %s", err)
|
log.Debugf("failed to create input chain jump rule: %s", err)
|
||||||
@@ -322,6 +336,13 @@ func (m *aclManager) createDefaultChains() error {
|
|||||||
}
|
}
|
||||||
clear(m.optionalEntries)
|
clear(m.optionalEntries)
|
||||||
|
|
||||||
|
// Insert mangle FORWARD guard rules to prevent external DNAT bypass.
|
||||||
|
for _, rule := range m.entries[mangleFwdKey] {
|
||||||
|
if err := m.iptablesClient.AppendUnique(tableMangle, chainFORWARD, rule...); err != nil {
|
||||||
|
log.Errorf("failed to add mangle FORWARD guard rule: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,6 +364,22 @@ func (m *aclManager) seedInitialEntries() {
|
|||||||
|
|
||||||
m.appendToEntries("FORWARD", []string{"-o", m.wgIface.Name(), "-j", chainRTFWDOUT})
|
m.appendToEntries("FORWARD", []string{"-o", m.wgIface.Name(), "-j", chainRTFWDOUT})
|
||||||
m.appendToEntries("FORWARD", []string{"-i", m.wgIface.Name(), "-j", chainRTFWDIN})
|
m.appendToEntries("FORWARD", []string{"-i", m.wgIface.Name(), "-j", chainRTFWDIN})
|
||||||
|
|
||||||
|
// Mangle FORWARD guard: when external DNAT redirects traffic from the wg interface, it
|
||||||
|
// traverses FORWARD instead of INPUT, bypassing ACL rules. ACCEPT rules in filter FORWARD
|
||||||
|
// can be inserted above ours. Mangle runs before filter, so these guard rules enforce the
|
||||||
|
// ACL mark check where it cannot be overridden.
|
||||||
|
m.appendToEntries(mangleFwdKey, []string{
|
||||||
|
"-i", m.wgIface.Name(),
|
||||||
|
"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED",
|
||||||
|
"-j", "ACCEPT",
|
||||||
|
})
|
||||||
|
m.appendToEntries(mangleFwdKey, []string{
|
||||||
|
"-i", m.wgIface.Name(),
|
||||||
|
"-m", "conntrack", "--ctstate", "DNAT",
|
||||||
|
"-m", "mark", "!", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected),
|
||||||
|
"-j", "DROP",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *aclManager) seedInitialOptionalEntries() {
|
func (m *aclManager) seedInitialOptionalEntries() {
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ type Manager struct {
|
|||||||
type iFaceMapper interface {
|
type iFaceMapper interface {
|
||||||
Name() string
|
Name() string
|
||||||
Address() wgaddr.Address
|
Address() wgaddr.Address
|
||||||
IsUserspaceBind() bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create iptables firewall manager
|
// Create iptables firewall manager
|
||||||
@@ -64,10 +63,9 @@ func Create(wgIface iFaceMapper, mtu uint16) (*Manager, error) {
|
|||||||
func (m *Manager) Init(stateManager *statemanager.Manager) error {
|
func (m *Manager) Init(stateManager *statemanager.Manager) error {
|
||||||
state := &ShutdownState{
|
state := &ShutdownState{
|
||||||
InterfaceState: &InterfaceState{
|
InterfaceState: &InterfaceState{
|
||||||
NameStr: m.wgIface.Name(),
|
NameStr: m.wgIface.Name(),
|
||||||
WGAddress: m.wgIface.Address(),
|
WGAddress: m.wgIface.Address(),
|
||||||
UserspaceBind: m.wgIface.IsUserspaceBind(),
|
MTU: m.router.mtu,
|
||||||
MTU: m.router.mtu,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
stateManager.RegisterState(state)
|
stateManager.RegisterState(state)
|
||||||
@@ -203,12 +201,10 @@ func (m *Manager) Close(stateManager *statemanager.Manager) error {
|
|||||||
return nberrors.FormatErrorOrNil(merr)
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllowNetbird allows netbird interface traffic
|
// AllowNetbird allows netbird interface traffic.
|
||||||
|
// This is called when USPFilter wraps the native firewall, adding blanket accept
|
||||||
|
// rules so that packet filtering is handled in userspace instead of by netfilter.
|
||||||
func (m *Manager) AllowNetbird() error {
|
func (m *Manager) AllowNetbird() error {
|
||||||
if !m.wgIface.IsUserspaceBind() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := m.AddPeerFiltering(
|
_, err := m.AddPeerFiltering(
|
||||||
nil,
|
nil,
|
||||||
net.IP{0, 0, 0, 0},
|
net.IP{0, 0, 0, 0},
|
||||||
@@ -286,6 +282,22 @@ func (m *Manager) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Prot
|
|||||||
return m.router.RemoveInboundDNAT(localAddr, protocol, sourcePort, targetPort)
|
return m.router.RemoveInboundDNAT(localAddr, protocol, sourcePort, targetPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddOutputDNAT adds an OUTPUT chain DNAT rule for locally-generated traffic.
|
||||||
|
func (m *Manager) AddOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
return m.router.AddOutputDNAT(localAddr, protocol, sourcePort, targetPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveOutputDNAT removes an OUTPUT chain DNAT rule.
|
||||||
|
func (m *Manager) RemoveOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
return m.router.RemoveOutputDNAT(localAddr, protocol, sourcePort, targetPort)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
chainNameRaw = "NETBIRD-RAW"
|
chainNameRaw = "NETBIRD-RAW"
|
||||||
chainOUTPUT = "OUTPUT"
|
chainOUTPUT = "OUTPUT"
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ func (i *iFaceMock) Address() wgaddr.Address {
|
|||||||
panic("AddressFunc is not set")
|
panic("AddressFunc is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *iFaceMock) IsUserspaceBind() bool { return false }
|
|
||||||
|
|
||||||
func TestIptablesManager(t *testing.T) {
|
func TestIptablesManager(t *testing.T) {
|
||||||
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
ipv4Client, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const (
|
|||||||
chainRTFWDOUT = "NETBIRD-RT-FWD-OUT"
|
chainRTFWDOUT = "NETBIRD-RT-FWD-OUT"
|
||||||
chainRTPRE = "NETBIRD-RT-PRE"
|
chainRTPRE = "NETBIRD-RT-PRE"
|
||||||
chainRTRDR = "NETBIRD-RT-RDR"
|
chainRTRDR = "NETBIRD-RT-RDR"
|
||||||
|
chainNATOutput = "NETBIRD-NAT-OUTPUT"
|
||||||
chainRTMSSCLAMP = "NETBIRD-RT-MSSCLAMP"
|
chainRTMSSCLAMP = "NETBIRD-RT-MSSCLAMP"
|
||||||
routingFinalForwardJump = "ACCEPT"
|
routingFinalForwardJump = "ACCEPT"
|
||||||
routingFinalNatJump = "MASQUERADE"
|
routingFinalNatJump = "MASQUERADE"
|
||||||
@@ -43,6 +44,7 @@ const (
|
|||||||
jumpManglePre = "jump-mangle-pre"
|
jumpManglePre = "jump-mangle-pre"
|
||||||
jumpNatPre = "jump-nat-pre"
|
jumpNatPre = "jump-nat-pre"
|
||||||
jumpNatPost = "jump-nat-post"
|
jumpNatPost = "jump-nat-post"
|
||||||
|
jumpNatOutput = "jump-nat-output"
|
||||||
jumpMSSClamp = "jump-mss-clamp"
|
jumpMSSClamp = "jump-mss-clamp"
|
||||||
markManglePre = "mark-mangle-pre"
|
markManglePre = "mark-mangle-pre"
|
||||||
markManglePost = "mark-mangle-post"
|
markManglePost = "mark-mangle-post"
|
||||||
@@ -387,6 +389,14 @@ func (r *router) cleanUpDefaultForwardRules() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("flushing routing related tables")
|
log.Debug("flushing routing related tables")
|
||||||
|
|
||||||
|
// Remove jump rules from built-in chains before deleting custom chains,
|
||||||
|
// otherwise the chain deletion fails with "device or resource busy".
|
||||||
|
jumpRule := []string{"-j", chainNATOutput}
|
||||||
|
if err := r.iptablesClient.Delete(tableNat, "OUTPUT", jumpRule...); err != nil {
|
||||||
|
log.Debugf("clean OUTPUT jump rule: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
for _, chainInfo := range []struct {
|
for _, chainInfo := range []struct {
|
||||||
chain string
|
chain string
|
||||||
table string
|
table string
|
||||||
@@ -396,6 +406,7 @@ func (r *router) cleanUpDefaultForwardRules() error {
|
|||||||
{chainRTPRE, tableMangle},
|
{chainRTPRE, tableMangle},
|
||||||
{chainRTNAT, tableNat},
|
{chainRTNAT, tableNat},
|
||||||
{chainRTRDR, tableNat},
|
{chainRTRDR, tableNat},
|
||||||
|
{chainNATOutput, tableNat},
|
||||||
{chainRTMSSCLAMP, tableMangle},
|
{chainRTMSSCLAMP, tableMangle},
|
||||||
} {
|
} {
|
||||||
ok, err := r.iptablesClient.ChainExists(chainInfo.table, chainInfo.chain)
|
ok, err := r.iptablesClient.ChainExists(chainInfo.table, chainInfo.chain)
|
||||||
@@ -970,6 +981,81 @@ func (r *router) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Proto
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensureNATOutputChain lazily creates the OUTPUT NAT chain and jump rule on first use.
|
||||||
|
func (r *router) ensureNATOutputChain() error {
|
||||||
|
if _, exists := r.rules[jumpNatOutput]; exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
chainExists, err := r.iptablesClient.ChainExists(tableNat, chainNATOutput)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("check chain %s: %w", chainNATOutput, err)
|
||||||
|
}
|
||||||
|
if !chainExists {
|
||||||
|
if err := r.iptablesClient.NewChain(tableNat, chainNATOutput); err != nil {
|
||||||
|
return fmt.Errorf("create chain %s: %w", chainNATOutput, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpRule := []string{"-j", chainNATOutput}
|
||||||
|
if err := r.iptablesClient.Insert(tableNat, "OUTPUT", 1, jumpRule...); err != nil {
|
||||||
|
if !chainExists {
|
||||||
|
if delErr := r.iptablesClient.ClearAndDeleteChain(tableNat, chainNATOutput); delErr != nil {
|
||||||
|
log.Warnf("failed to rollback chain %s: %v", chainNATOutput, delErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("add OUTPUT jump rule: %w", err)
|
||||||
|
}
|
||||||
|
r.rules[jumpNatOutput] = jumpRule
|
||||||
|
|
||||||
|
r.updateState()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddOutputDNAT adds an OUTPUT chain DNAT rule for locally-generated traffic.
|
||||||
|
func (r *router) AddOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
||||||
|
ruleID := fmt.Sprintf("output-dnat-%s-%s-%d-%d", localAddr.String(), protocol, sourcePort, targetPort)
|
||||||
|
|
||||||
|
if _, exists := r.rules[ruleID]; exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.ensureNATOutputChain(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dnatRule := []string{
|
||||||
|
"-p", strings.ToLower(string(protocol)),
|
||||||
|
"--dport", strconv.Itoa(int(sourcePort)),
|
||||||
|
"-d", localAddr.String(),
|
||||||
|
"-j", "DNAT",
|
||||||
|
"--to-destination", ":" + strconv.Itoa(int(targetPort)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.iptablesClient.Append(tableNat, chainNATOutput, dnatRule...); err != nil {
|
||||||
|
return fmt.Errorf("add output DNAT rule: %w", err)
|
||||||
|
}
|
||||||
|
r.rules[ruleID] = dnatRule
|
||||||
|
|
||||||
|
r.updateState()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveOutputDNAT removes an OUTPUT chain DNAT rule.
|
||||||
|
func (r *router) RemoveOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
||||||
|
ruleID := fmt.Sprintf("output-dnat-%s-%s-%d-%d", localAddr.String(), protocol, sourcePort, targetPort)
|
||||||
|
|
||||||
|
if dnatRule, exists := r.rules[ruleID]; exists {
|
||||||
|
if err := r.iptablesClient.Delete(tableNat, chainNATOutput, dnatRule...); err != nil {
|
||||||
|
return fmt.Errorf("delete output DNAT rule: %w", err)
|
||||||
|
}
|
||||||
|
delete(r.rules, ruleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.updateState()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func applyPort(flag string, port *firewall.Port) []string {
|
func applyPort(flag string, port *firewall.Port) []string {
|
||||||
if port == nil {
|
if port == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -9,10 +9,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type InterfaceState struct {
|
type InterfaceState struct {
|
||||||
NameStr string `json:"name"`
|
NameStr string `json:"name"`
|
||||||
WGAddress wgaddr.Address `json:"wg_address"`
|
WGAddress wgaddr.Address `json:"wg_address"`
|
||||||
UserspaceBind bool `json:"userspace_bind"`
|
MTU uint16 `json:"mtu"`
|
||||||
MTU uint16 `json:"mtu"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *InterfaceState) Name() string {
|
func (i *InterfaceState) Name() string {
|
||||||
@@ -23,10 +22,6 @@ func (i *InterfaceState) Address() wgaddr.Address {
|
|||||||
return i.WGAddress
|
return i.WGAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *InterfaceState) IsUserspaceBind() bool {
|
|
||||||
return i.UserspaceBind
|
|
||||||
}
|
|
||||||
|
|
||||||
type ShutdownState struct {
|
type ShutdownState struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
|
|
||||||
|
|||||||
@@ -169,6 +169,14 @@ type Manager interface {
|
|||||||
// RemoveInboundDNAT removes inbound DNAT rule
|
// RemoveInboundDNAT removes inbound DNAT rule
|
||||||
RemoveInboundDNAT(localAddr netip.Addr, protocol Protocol, sourcePort, targetPort uint16) error
|
RemoveInboundDNAT(localAddr netip.Addr, protocol Protocol, sourcePort, targetPort uint16) error
|
||||||
|
|
||||||
|
// AddOutputDNAT adds an OUTPUT chain DNAT rule for locally-generated traffic.
|
||||||
|
// localAddr must be IPv4; the underlying iptables/nftables backends are IPv4-only.
|
||||||
|
AddOutputDNAT(localAddr netip.Addr, protocol Protocol, sourcePort, targetPort uint16) error
|
||||||
|
|
||||||
|
// RemoveOutputDNAT removes an OUTPUT chain DNAT rule.
|
||||||
|
// localAddr must be IPv4; the underlying iptables/nftables backends are IPv4-only.
|
||||||
|
RemoveOutputDNAT(localAddr netip.Addr, protocol Protocol, sourcePort, targetPort uint16) error
|
||||||
|
|
||||||
// SetupEBPFProxyNoTrack creates static notrack rules for eBPF proxy loopback traffic.
|
// SetupEBPFProxyNoTrack creates static notrack rules for eBPF proxy loopback traffic.
|
||||||
// This prevents conntrack from interfering with WireGuard proxy communication.
|
// This prevents conntrack from interfering with WireGuard proxy communication.
|
||||||
SetupEBPFProxyNoTrack(proxyPort, wgPort uint16) error
|
SetupEBPFProxyNoTrack(proxyPort, wgPort uint16) error
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ func getTableName() string {
|
|||||||
type iFaceMapper interface {
|
type iFaceMapper interface {
|
||||||
Name() string
|
Name() string
|
||||||
Address() wgaddr.Address
|
Address() wgaddr.Address
|
||||||
IsUserspaceBind() bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manager of iptables firewall
|
// Manager of iptables firewall
|
||||||
@@ -106,10 +105,9 @@ func (m *Manager) Init(stateManager *statemanager.Manager) error {
|
|||||||
// cleanup using Close() without needing to store specific rules.
|
// cleanup using Close() without needing to store specific rules.
|
||||||
if err := stateManager.UpdateState(&ShutdownState{
|
if err := stateManager.UpdateState(&ShutdownState{
|
||||||
InterfaceState: &InterfaceState{
|
InterfaceState: &InterfaceState{
|
||||||
NameStr: m.wgIface.Name(),
|
NameStr: m.wgIface.Name(),
|
||||||
WGAddress: m.wgIface.Address(),
|
WGAddress: m.wgIface.Address(),
|
||||||
UserspaceBind: m.wgIface.IsUserspaceBind(),
|
MTU: m.router.mtu,
|
||||||
MTU: m.router.mtu,
|
|
||||||
},
|
},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Errorf("failed to update state: %v", err)
|
log.Errorf("failed to update state: %v", err)
|
||||||
@@ -205,12 +203,10 @@ func (m *Manager) RemoveNatRule(pair firewall.RouterPair) error {
|
|||||||
return m.router.RemoveNatRule(pair)
|
return m.router.RemoveNatRule(pair)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AllowNetbird allows netbird interface traffic
|
// AllowNetbird allows netbird interface traffic.
|
||||||
|
// This is called when USPFilter wraps the native firewall, adding blanket accept
|
||||||
|
// rules so that packet filtering is handled in userspace instead of by netfilter.
|
||||||
func (m *Manager) AllowNetbird() error {
|
func (m *Manager) AllowNetbird() error {
|
||||||
if !m.wgIface.IsUserspaceBind() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
@@ -346,6 +342,22 @@ func (m *Manager) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Prot
|
|||||||
return m.router.RemoveInboundDNAT(localAddr, protocol, sourcePort, targetPort)
|
return m.router.RemoveInboundDNAT(localAddr, protocol, sourcePort, targetPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddOutputDNAT adds an OUTPUT chain DNAT rule for locally-generated traffic.
|
||||||
|
func (m *Manager) AddOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
return m.router.AddOutputDNAT(localAddr, protocol, sourcePort, targetPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveOutputDNAT removes an OUTPUT chain DNAT rule.
|
||||||
|
func (m *Manager) RemoveOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
return m.router.RemoveOutputDNAT(localAddr, protocol, sourcePort, targetPort)
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
chainNameRawOutput = "netbird-raw-out"
|
chainNameRawOutput = "netbird-raw-out"
|
||||||
chainNameRawPrerouting = "netbird-raw-pre"
|
chainNameRawPrerouting = "netbird-raw-pre"
|
||||||
|
|||||||
@@ -52,8 +52,6 @@ func (i *iFaceMock) Address() wgaddr.Address {
|
|||||||
panic("AddressFunc is not set")
|
panic("AddressFunc is not set")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *iFaceMock) IsUserspaceBind() bool { return false }
|
|
||||||
|
|
||||||
func TestNftablesManager(t *testing.T) {
|
func TestNftablesManager(t *testing.T) {
|
||||||
|
|
||||||
// just check on the local interface
|
// just check on the local interface
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const (
|
|||||||
chainNameRoutingFw = "netbird-rt-fwd"
|
chainNameRoutingFw = "netbird-rt-fwd"
|
||||||
chainNameRoutingNat = "netbird-rt-postrouting"
|
chainNameRoutingNat = "netbird-rt-postrouting"
|
||||||
chainNameRoutingRdr = "netbird-rt-redirect"
|
chainNameRoutingRdr = "netbird-rt-redirect"
|
||||||
|
chainNameNATOutput = "netbird-nat-output"
|
||||||
chainNameForward = "FORWARD"
|
chainNameForward = "FORWARD"
|
||||||
chainNameMangleForward = "netbird-mangle-forward"
|
chainNameMangleForward = "netbird-mangle-forward"
|
||||||
|
|
||||||
@@ -1853,6 +1854,130 @@ func (r *router) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Proto
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensureNATOutputChain lazily creates the OUTPUT NAT chain on first use.
|
||||||
|
func (r *router) ensureNATOutputChain() error {
|
||||||
|
if _, exists := r.chains[chainNameNATOutput]; exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.chains[chainNameNATOutput] = r.conn.AddChain(&nftables.Chain{
|
||||||
|
Name: chainNameNATOutput,
|
||||||
|
Table: r.workTable,
|
||||||
|
Hooknum: nftables.ChainHookOutput,
|
||||||
|
Priority: nftables.ChainPriorityNATDest,
|
||||||
|
Type: nftables.ChainTypeNAT,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := r.conn.Flush(); err != nil {
|
||||||
|
delete(r.chains, chainNameNATOutput)
|
||||||
|
return fmt.Errorf("create NAT output chain: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddOutputDNAT adds an OUTPUT chain DNAT rule for locally-generated traffic.
|
||||||
|
func (r *router) AddOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
||||||
|
ruleID := fmt.Sprintf("output-dnat-%s-%s-%d-%d", localAddr.String(), protocol, sourcePort, targetPort)
|
||||||
|
|
||||||
|
if _, exists := r.rules[ruleID]; exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.ensureNATOutputChain(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
protoNum, err := protoToInt(protocol)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("convert protocol to number: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exprs := []expr.Any{
|
||||||
|
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpEq,
|
||||||
|
Register: 1,
|
||||||
|
Data: []byte{protoNum},
|
||||||
|
},
|
||||||
|
&expr.Payload{
|
||||||
|
DestRegister: 2,
|
||||||
|
Base: expr.PayloadBaseTransportHeader,
|
||||||
|
Offset: 2,
|
||||||
|
Len: 2,
|
||||||
|
},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpEq,
|
||||||
|
Register: 2,
|
||||||
|
Data: binaryutil.BigEndian.PutUint16(sourcePort),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
exprs = append(exprs, applyPrefix(netip.PrefixFrom(localAddr, 32), false)...)
|
||||||
|
|
||||||
|
exprs = append(exprs,
|
||||||
|
&expr.Immediate{
|
||||||
|
Register: 1,
|
||||||
|
Data: localAddr.AsSlice(),
|
||||||
|
},
|
||||||
|
&expr.Immediate{
|
||||||
|
Register: 2,
|
||||||
|
Data: binaryutil.BigEndian.PutUint16(targetPort),
|
||||||
|
},
|
||||||
|
&expr.NAT{
|
||||||
|
Type: expr.NATTypeDestNAT,
|
||||||
|
Family: uint32(nftables.TableFamilyIPv4),
|
||||||
|
RegAddrMin: 1,
|
||||||
|
RegProtoMin: 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
dnatRule := &nftables.Rule{
|
||||||
|
Table: r.workTable,
|
||||||
|
Chain: r.chains[chainNameNATOutput],
|
||||||
|
Exprs: exprs,
|
||||||
|
UserData: []byte(ruleID),
|
||||||
|
}
|
||||||
|
r.conn.AddRule(dnatRule)
|
||||||
|
|
||||||
|
if err := r.conn.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("add output DNAT rule: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.rules[ruleID] = dnatRule
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveOutputDNAT removes an OUTPUT chain DNAT rule.
|
||||||
|
func (r *router) RemoveOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
||||||
|
if err := r.refreshRulesMap(); err != nil {
|
||||||
|
return fmt.Errorf(refreshRulesMapError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ruleID := fmt.Sprintf("output-dnat-%s-%s-%d-%d", localAddr.String(), protocol, sourcePort, targetPort)
|
||||||
|
|
||||||
|
rule, exists := r.rules[ruleID]
|
||||||
|
if !exists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.Handle == 0 {
|
||||||
|
log.Warnf("output DNAT rule %s has no handle, removing stale entry", ruleID)
|
||||||
|
delete(r.rules, ruleID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r.conn.DelRule(rule); err != nil {
|
||||||
|
return fmt.Errorf("delete output DNAT rule %s: %w", ruleID, err)
|
||||||
|
}
|
||||||
|
if err := r.conn.Flush(); err != nil {
|
||||||
|
return fmt.Errorf("flush delete output DNAT rule: %w", err)
|
||||||
|
}
|
||||||
|
delete(r.rules, ruleID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// applyNetwork generates nftables expressions for networks (CIDR) or sets
|
// applyNetwork generates nftables expressions for networks (CIDR) or sets
|
||||||
func (r *router) applyNetwork(
|
func (r *router) applyNetwork(
|
||||||
network firewall.Network,
|
network firewall.Network,
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type InterfaceState struct {
|
type InterfaceState struct {
|
||||||
NameStr string `json:"name"`
|
NameStr string `json:"name"`
|
||||||
WGAddress wgaddr.Address `json:"wg_address"`
|
WGAddress wgaddr.Address `json:"wg_address"`
|
||||||
UserspaceBind bool `json:"userspace_bind"`
|
MTU uint16 `json:"mtu"`
|
||||||
MTU uint16 `json:"mtu"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *InterfaceState) Name() string {
|
func (i *InterfaceState) Name() string {
|
||||||
@@ -22,10 +21,6 @@ func (i *InterfaceState) Address() wgaddr.Address {
|
|||||||
return i.WGAddress
|
return i.WGAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *InterfaceState) IsUserspaceBind() bool {
|
|
||||||
return i.UserspaceBind
|
|
||||||
}
|
|
||||||
|
|
||||||
type ShutdownState struct {
|
type ShutdownState struct {
|
||||||
InterfaceState *InterfaceState `json:"interface_state,omitempty"`
|
InterfaceState *InterfaceState `json:"interface_state,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
37
client/firewall/uspfilter/common/hooks.go
Normal file
37
client/firewall/uspfilter/common/hooks.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PacketHook stores a registered hook for a specific IP:port.
|
||||||
|
type PacketHook struct {
|
||||||
|
IP netip.Addr
|
||||||
|
Port uint16
|
||||||
|
Fn func([]byte) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// HookMatches checks if a packet's destination matches the hook and invokes it.
|
||||||
|
func HookMatches(h *PacketHook, dstIP netip.Addr, dport uint16, packetData []byte) bool {
|
||||||
|
if h == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if h.IP == dstIP && h.Port == dport {
|
||||||
|
return h.Fn(packetData)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetHook atomically stores a hook, handling nil removal.
|
||||||
|
func SetHook(ptr *atomic.Pointer[PacketHook], ip netip.Addr, dPort uint16, hook func([]byte) bool) {
|
||||||
|
if hook == nil {
|
||||||
|
ptr.Store(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ptr.Store(&PacketHook{
|
||||||
|
IP: ip,
|
||||||
|
Port: dPort,
|
||||||
|
Fn: hook,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -140,6 +140,10 @@ type Manager struct {
|
|||||||
mtu uint16
|
mtu uint16
|
||||||
mssClampValue uint16
|
mssClampValue uint16
|
||||||
mssClampEnabled bool
|
mssClampEnabled bool
|
||||||
|
|
||||||
|
// Only one hook per protocol is supported. Outbound direction only.
|
||||||
|
udpHookOut atomic.Pointer[common.PacketHook]
|
||||||
|
tcpHookOut atomic.Pointer[common.PacketHook]
|
||||||
}
|
}
|
||||||
|
|
||||||
// decoder for packages
|
// decoder for packages
|
||||||
@@ -594,6 +598,8 @@ func (m *Manager) resetState() {
|
|||||||
maps.Clear(m.incomingRules)
|
maps.Clear(m.incomingRules)
|
||||||
maps.Clear(m.routeRulesMap)
|
maps.Clear(m.routeRulesMap)
|
||||||
m.routeRules = m.routeRules[:0]
|
m.routeRules = m.routeRules[:0]
|
||||||
|
m.udpHookOut.Store(nil)
|
||||||
|
m.tcpHookOut.Store(nil)
|
||||||
|
|
||||||
if m.udpTracker != nil {
|
if m.udpTracker != nil {
|
||||||
m.udpTracker.Close()
|
m.udpTracker.Close()
|
||||||
@@ -713,6 +719,9 @@ func (m *Manager) filterOutbound(packetData []byte, size int) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
case layers.LayerTypeTCP:
|
case layers.LayerTypeTCP:
|
||||||
|
if m.tcpHooksDrop(uint16(d.tcp.DstPort), dstIP, packetData) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
// Clamp MSS on all TCP SYN packets, including those from local IPs.
|
// Clamp MSS on all TCP SYN packets, including those from local IPs.
|
||||||
// SNATed routed traffic may appear as local IP but still requires clamping.
|
// SNATed routed traffic may appear as local IP but still requires clamping.
|
||||||
if m.mssClampEnabled {
|
if m.mssClampEnabled {
|
||||||
@@ -895,39 +904,12 @@ func (m *Manager) trackInbound(d *decoder, srcIP, dstIP netip.Addr, ruleID []byt
|
|||||||
d.dnatOrigPort = 0
|
d.dnatOrigPort = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// udpHooksDrop checks if any UDP hooks should drop the packet
|
|
||||||
func (m *Manager) udpHooksDrop(dport uint16, dstIP netip.Addr, packetData []byte) bool {
|
func (m *Manager) udpHooksDrop(dport uint16, dstIP netip.Addr, packetData []byte) bool {
|
||||||
m.mutex.RLock()
|
return common.HookMatches(m.udpHookOut.Load(), dstIP, dport, packetData)
|
||||||
defer m.mutex.RUnlock()
|
}
|
||||||
|
|
||||||
// Check specific destination IP first
|
func (m *Manager) tcpHooksDrop(dport uint16, dstIP netip.Addr, packetData []byte) bool {
|
||||||
if rules, exists := m.outgoingRules[dstIP]; exists {
|
return common.HookMatches(m.tcpHookOut.Load(), dstIP, dport, packetData)
|
||||||
for _, rule := range rules {
|
|
||||||
if rule.udpHook != nil && portsMatch(rule.dPort, dport) {
|
|
||||||
return rule.udpHook(packetData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check IPv4 unspecified address
|
|
||||||
if rules, exists := m.outgoingRules[netip.IPv4Unspecified()]; exists {
|
|
||||||
for _, rule := range rules {
|
|
||||||
if rule.udpHook != nil && portsMatch(rule.dPort, dport) {
|
|
||||||
return rule.udpHook(packetData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check IPv6 unspecified address
|
|
||||||
if rules, exists := m.outgoingRules[netip.IPv6Unspecified()]; exists {
|
|
||||||
for _, rule := range rules {
|
|
||||||
if rule.udpHook != nil && portsMatch(rule.dPort, dport) {
|
|
||||||
return rule.udpHook(packetData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// filterInbound implements filtering logic for incoming packets.
|
// filterInbound implements filtering logic for incoming packets.
|
||||||
@@ -1278,12 +1260,6 @@ func validateRule(ip netip.Addr, packetData []byte, rules map[string]PeerRule, d
|
|||||||
return rule.mgmtId, rule.drop, true
|
return rule.mgmtId, rule.drop, true
|
||||||
}
|
}
|
||||||
case layers.LayerTypeUDP:
|
case layers.LayerTypeUDP:
|
||||||
// if rule has UDP hook (and if we are here we match this rule)
|
|
||||||
// we ignore rule.drop and call this hook
|
|
||||||
if rule.udpHook != nil {
|
|
||||||
return rule.mgmtId, rule.udpHook(packetData), true
|
|
||||||
}
|
|
||||||
|
|
||||||
if portsMatch(rule.sPort, uint16(d.udp.SrcPort)) && portsMatch(rule.dPort, uint16(d.udp.DstPort)) {
|
if portsMatch(rule.sPort, uint16(d.udp.SrcPort)) && portsMatch(rule.dPort, uint16(d.udp.DstPort)) {
|
||||||
return rule.mgmtId, rule.drop, true
|
return rule.mgmtId, rule.drop, true
|
||||||
}
|
}
|
||||||
@@ -1342,65 +1318,14 @@ func (m *Manager) ruleMatches(rule *RouteRule, srcAddr, dstAddr netip.Addr, prot
|
|||||||
return sourceMatched
|
return sourceMatched
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddUDPPacketHook calls hook when UDP packet from given direction matched
|
// SetUDPPacketHook sets the outbound UDP packet hook. Pass nil hook to remove.
|
||||||
//
|
func (m *Manager) SetUDPPacketHook(ip netip.Addr, dPort uint16, hook func(packet []byte) bool) {
|
||||||
// Hook function returns flag which indicates should be the matched package dropped or not
|
common.SetHook(&m.udpHookOut, ip, dPort, hook)
|
||||||
func (m *Manager) AddUDPPacketHook(in bool, ip netip.Addr, dPort uint16, hook func(packet []byte) bool) string {
|
|
||||||
r := PeerRule{
|
|
||||||
id: uuid.New().String(),
|
|
||||||
ip: ip,
|
|
||||||
protoLayer: layers.LayerTypeUDP,
|
|
||||||
dPort: &firewall.Port{Values: []uint16{dPort}},
|
|
||||||
ipLayer: layers.LayerTypeIPv6,
|
|
||||||
udpHook: hook,
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip.Is4() {
|
|
||||||
r.ipLayer = layers.LayerTypeIPv4
|
|
||||||
}
|
|
||||||
|
|
||||||
m.mutex.Lock()
|
|
||||||
if in {
|
|
||||||
// Incoming UDP hooks are stored in allow rules map
|
|
||||||
if _, ok := m.incomingRules[r.ip]; !ok {
|
|
||||||
m.incomingRules[r.ip] = make(map[string]PeerRule)
|
|
||||||
}
|
|
||||||
m.incomingRules[r.ip][r.id] = r
|
|
||||||
} else {
|
|
||||||
if _, ok := m.outgoingRules[r.ip]; !ok {
|
|
||||||
m.outgoingRules[r.ip] = make(map[string]PeerRule)
|
|
||||||
}
|
|
||||||
m.outgoingRules[r.ip][r.id] = r
|
|
||||||
}
|
|
||||||
m.mutex.Unlock()
|
|
||||||
|
|
||||||
return r.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemovePacketHook removes packet hook by given ID
|
// SetTCPPacketHook sets the outbound TCP packet hook. Pass nil hook to remove.
|
||||||
func (m *Manager) RemovePacketHook(hookID string) error {
|
func (m *Manager) SetTCPPacketHook(ip netip.Addr, dPort uint16, hook func(packet []byte) bool) {
|
||||||
m.mutex.Lock()
|
common.SetHook(&m.tcpHookOut, ip, dPort, hook)
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
// Check incoming hooks (stored in allow rules)
|
|
||||||
for _, arr := range m.incomingRules {
|
|
||||||
for _, r := range arr {
|
|
||||||
if r.id == hookID {
|
|
||||||
delete(arr, r.id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check outgoing hooks
|
|
||||||
for _, arr := range m.outgoingRules {
|
|
||||||
for _, r := range arr {
|
|
||||||
if r.id == hookID {
|
|
||||||
delete(arr, r.id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("hook with given id not found")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLogLevel sets the log level for the firewall manager
|
// SetLogLevel sets the log level for the firewall manager
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/google/gopacket"
|
"github.com/google/gopacket"
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
wgdevice "golang.zx2c4.com/wireguard/device"
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
|
||||||
@@ -186,81 +187,52 @@ func TestManagerDeleteRule(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddUDPPacketHook(t *testing.T) {
|
func TestSetUDPPacketHook(t *testing.T) {
|
||||||
tests := []struct {
|
manager, err := Create(&IFaceMock{
|
||||||
name string
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
in bool
|
}, false, flowLogger, nbiface.DefaultMTU)
|
||||||
expDir fw.RuleDirection
|
require.NoError(t, err)
|
||||||
ip netip.Addr
|
t.Cleanup(func() { require.NoError(t, manager.Close(nil)) })
|
||||||
dPort uint16
|
|
||||||
hook func([]byte) bool
|
|
||||||
expectedID string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Test Outgoing UDP Packet Hook",
|
|
||||||
in: false,
|
|
||||||
expDir: fw.RuleDirectionOUT,
|
|
||||||
ip: netip.MustParseAddr("10.168.0.1"),
|
|
||||||
dPort: 8000,
|
|
||||||
hook: func([]byte) bool { return true },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Test Incoming UDP Packet Hook",
|
|
||||||
in: true,
|
|
||||||
expDir: fw.RuleDirectionIN,
|
|
||||||
ip: netip.MustParseAddr("::1"),
|
|
||||||
dPort: 9000,
|
|
||||||
hook: func([]byte) bool { return false },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
var called bool
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
manager.SetUDPPacketHook(netip.MustParseAddr("10.168.0.1"), 8000, func([]byte) bool {
|
||||||
manager, err := Create(&IFaceMock{
|
called = true
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
return true
|
||||||
}, false, flowLogger, nbiface.DefaultMTU)
|
})
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
manager.AddUDPPacketHook(tt.in, tt.ip, tt.dPort, tt.hook)
|
h := manager.udpHookOut.Load()
|
||||||
|
require.NotNil(t, h)
|
||||||
|
assert.Equal(t, netip.MustParseAddr("10.168.0.1"), h.IP)
|
||||||
|
assert.Equal(t, uint16(8000), h.Port)
|
||||||
|
assert.True(t, h.Fn(nil))
|
||||||
|
assert.True(t, called)
|
||||||
|
|
||||||
var addedRule PeerRule
|
manager.SetUDPPacketHook(netip.MustParseAddr("10.168.0.1"), 8000, nil)
|
||||||
if tt.in {
|
assert.Nil(t, manager.udpHookOut.Load())
|
||||||
// Incoming UDP hooks are stored in allow rules map
|
}
|
||||||
if len(manager.incomingRules[tt.ip]) != 1 {
|
|
||||||
t.Errorf("expected 1 incoming rule, got %d", len(manager.incomingRules[tt.ip]))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, rule := range manager.incomingRules[tt.ip] {
|
|
||||||
addedRule = rule
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if len(manager.outgoingRules[tt.ip]) != 1 {
|
|
||||||
t.Errorf("expected 1 outgoing rule, got %d", len(manager.outgoingRules[tt.ip]))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, rule := range manager.outgoingRules[tt.ip] {
|
|
||||||
addedRule = rule
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.ip.Compare(addedRule.ip) != 0 {
|
func TestSetTCPPacketHook(t *testing.T) {
|
||||||
t.Errorf("expected ip %s, got %s", tt.ip, addedRule.ip)
|
manager, err := Create(&IFaceMock{
|
||||||
return
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}
|
}, false, flowLogger, nbiface.DefaultMTU)
|
||||||
if tt.dPort != addedRule.dPort.Values[0] {
|
require.NoError(t, err)
|
||||||
t.Errorf("expected dPort %d, got %d", tt.dPort, addedRule.dPort.Values[0])
|
t.Cleanup(func() { require.NoError(t, manager.Close(nil)) })
|
||||||
return
|
|
||||||
}
|
var called bool
|
||||||
if layers.LayerTypeUDP != addedRule.protoLayer {
|
manager.SetTCPPacketHook(netip.MustParseAddr("10.168.0.1"), 53, func([]byte) bool {
|
||||||
t.Errorf("expected protoLayer %s, got %s", layers.LayerTypeUDP, addedRule.protoLayer)
|
called = true
|
||||||
return
|
return true
|
||||||
}
|
})
|
||||||
if addedRule.udpHook == nil {
|
|
||||||
t.Errorf("expected udpHook to be set")
|
h := manager.tcpHookOut.Load()
|
||||||
return
|
require.NotNil(t, h)
|
||||||
}
|
assert.Equal(t, netip.MustParseAddr("10.168.0.1"), h.IP)
|
||||||
})
|
assert.Equal(t, uint16(53), h.Port)
|
||||||
}
|
assert.True(t, h.Fn(nil))
|
||||||
|
assert.True(t, called)
|
||||||
|
|
||||||
|
manager.SetTCPPacketHook(netip.MustParseAddr("10.168.0.1"), 53, nil)
|
||||||
|
assert.Nil(t, manager.tcpHookOut.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPeerRuleLifecycleDenyRules verifies that deny rules are correctly added
|
// TestPeerRuleLifecycleDenyRules verifies that deny rules are correctly added
|
||||||
@@ -530,39 +502,12 @@ func TestRemovePacketHook(t *testing.T) {
|
|||||||
require.NoError(t, manager.Close(nil))
|
require.NoError(t, manager.Close(nil))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Add a UDP packet hook
|
manager.SetUDPPacketHook(netip.MustParseAddr("192.168.0.1"), 8080, func([]byte) bool { return true })
|
||||||
hookFunc := func(data []byte) bool { return true }
|
|
||||||
hookID := manager.AddUDPPacketHook(false, netip.MustParseAddr("192.168.0.1"), 8080, hookFunc)
|
|
||||||
|
|
||||||
// Assert the hook is added by finding it in the manager's outgoing rules
|
require.NotNil(t, manager.udpHookOut.Load(), "hook should be registered")
|
||||||
found := false
|
|
||||||
for _, arr := range manager.outgoingRules {
|
|
||||||
for _, rule := range arr {
|
|
||||||
if rule.id == hookID {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
manager.SetUDPPacketHook(netip.MustParseAddr("192.168.0.1"), 8080, nil)
|
||||||
t.Fatalf("The hook was not added properly.")
|
assert.Nil(t, manager.udpHookOut.Load(), "hook should be removed")
|
||||||
}
|
|
||||||
|
|
||||||
// Now remove the packet hook
|
|
||||||
err = manager.RemovePacketHook(hookID)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Failed to remove hook: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert the hook is removed by checking it in the manager's outgoing rules
|
|
||||||
for _, arr := range manager.outgoingRules {
|
|
||||||
for _, rule := range arr {
|
|
||||||
if rule.id == hookID {
|
|
||||||
t.Fatalf("The hook was not removed properly.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProcessOutgoingHooks(t *testing.T) {
|
func TestProcessOutgoingHooks(t *testing.T) {
|
||||||
@@ -592,8 +537,7 @@ func TestProcessOutgoingHooks(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hookCalled := false
|
hookCalled := false
|
||||||
hookID := manager.AddUDPPacketHook(
|
manager.SetUDPPacketHook(
|
||||||
false,
|
|
||||||
netip.MustParseAddr("100.10.0.100"),
|
netip.MustParseAddr("100.10.0.100"),
|
||||||
53,
|
53,
|
||||||
func([]byte) bool {
|
func([]byte) bool {
|
||||||
@@ -601,7 +545,6 @@ func TestProcessOutgoingHooks(t *testing.T) {
|
|||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
require.NotEmpty(t, hookID)
|
|
||||||
|
|
||||||
// Create test UDP packet
|
// Create test UDP packet
|
||||||
ipv4 := &layers.IPv4{
|
ipv4 := &layers.IPv4{
|
||||||
|
|||||||
90
client/firewall/uspfilter/hooks_filter.go
Normal file
90
client/firewall/uspfilter/hooks_filter.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package uspfilter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"net/netip"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/common"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ipv4HeaderMinLen = 20
|
||||||
|
ipv4ProtoOffset = 9
|
||||||
|
ipv4FlagsOffset = 6
|
||||||
|
ipv4DstOffset = 16
|
||||||
|
ipProtoUDP = 17
|
||||||
|
ipProtoTCP = 6
|
||||||
|
ipv4FragOffMask = 0x1fff
|
||||||
|
// dstPortOffset is the offset of the destination port within a UDP or TCP header.
|
||||||
|
dstPortOffset = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// HooksFilter is a minimal packet filter that only handles outbound DNS hooks.
|
||||||
|
// It is installed on the WireGuard interface when the userspace bind is active
|
||||||
|
// but a full firewall filter (Manager) is not needed because a native kernel
|
||||||
|
// firewall (nftables/iptables) handles packet filtering.
|
||||||
|
type HooksFilter struct {
|
||||||
|
udpHook atomic.Pointer[common.PacketHook]
|
||||||
|
tcpHook atomic.Pointer[common.PacketHook]
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ device.PacketFilter = (*HooksFilter)(nil)
|
||||||
|
|
||||||
|
// FilterOutbound checks outbound packets for DNS hook matches.
|
||||||
|
// Only IPv4 packets matching the registered hook IP:port are intercepted.
|
||||||
|
// IPv6 and non-IP packets pass through unconditionally.
|
||||||
|
func (f *HooksFilter) FilterOutbound(packetData []byte, _ int) bool {
|
||||||
|
if len(packetData) < ipv4HeaderMinLen {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only process IPv4 packets, let everything else pass through.
|
||||||
|
if packetData[0]>>4 != 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ihl := int(packetData[0]&0x0f) * 4
|
||||||
|
if ihl < ipv4HeaderMinLen || len(packetData) < ihl+4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip non-first fragments: they don't carry L4 headers.
|
||||||
|
flagsAndOffset := binary.BigEndian.Uint16(packetData[ipv4FlagsOffset : ipv4FlagsOffset+2])
|
||||||
|
if flagsAndOffset&ipv4FragOffMask != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
dstIP, ok := netip.AddrFromSlice(packetData[ipv4DstOffset : ipv4DstOffset+4])
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
proto := packetData[ipv4ProtoOffset]
|
||||||
|
dstPort := binary.BigEndian.Uint16(packetData[ihl+dstPortOffset : ihl+dstPortOffset+2])
|
||||||
|
|
||||||
|
switch proto {
|
||||||
|
case ipProtoUDP:
|
||||||
|
return common.HookMatches(f.udpHook.Load(), dstIP, dstPort, packetData)
|
||||||
|
case ipProtoTCP:
|
||||||
|
return common.HookMatches(f.tcpHook.Load(), dstIP, dstPort, packetData)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterInbound allows all inbound packets (native firewall handles filtering).
|
||||||
|
func (f *HooksFilter) FilterInbound([]byte, int) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUDPPacketHook registers the UDP packet hook.
|
||||||
|
func (f *HooksFilter) SetUDPPacketHook(ip netip.Addr, dPort uint16, hook func([]byte) bool) {
|
||||||
|
common.SetHook(&f.udpHook, ip, dPort, hook)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCPPacketHook registers the TCP packet hook.
|
||||||
|
func (f *HooksFilter) SetTCPPacketHook(ip netip.Addr, dPort uint16, hook func([]byte) bool) {
|
||||||
|
common.SetHook(&f.tcpHook, ip, dPort, hook)
|
||||||
|
}
|
||||||
@@ -144,6 +144,8 @@ func (m *localIPManager) UpdateLocalIPs(iface common.IFaceMapper) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to get interfaces: %v", err)
|
log.Warnf("failed to get interfaces: %v", err)
|
||||||
} else {
|
} else {
|
||||||
|
// TODO: filter out down interfaces (net.FlagUp). Also handle the reverse
|
||||||
|
// case where an interface comes up between refreshes.
|
||||||
for _, intf := range interfaces {
|
for _, intf := range interfaces {
|
||||||
m.processInterface(intf, &newIPv4Bitmap, ipv4Set, &ipv4Addresses)
|
m.processInterface(intf, &newIPv4Bitmap, ipv4Set, &ipv4Addresses)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -421,6 +421,7 @@ func (m *Manager) addPortRedirection(targetIP netip.Addr, protocol gopacket.Laye
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddInboundDNAT adds an inbound DNAT rule redirecting traffic from NetBird peers to local services.
|
// AddInboundDNAT adds an inbound DNAT rule redirecting traffic from NetBird peers to local services.
|
||||||
|
// TODO: also delegate to nativeFirewall when available for kernel WG mode
|
||||||
func (m *Manager) AddInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
func (m *Manager) AddInboundDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
||||||
var layerType gopacket.LayerType
|
var layerType gopacket.LayerType
|
||||||
switch protocol {
|
switch protocol {
|
||||||
@@ -466,6 +467,22 @@ func (m *Manager) RemoveInboundDNAT(localAddr netip.Addr, protocol firewall.Prot
|
|||||||
return m.removePortRedirection(localAddr, layerType, sourcePort, targetPort)
|
return m.removePortRedirection(localAddr, layerType, sourcePort, targetPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AddOutputDNAT delegates to the native firewall if available.
|
||||||
|
func (m *Manager) AddOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
||||||
|
if m.nativeFirewall == nil {
|
||||||
|
return fmt.Errorf("output DNAT not supported without native firewall")
|
||||||
|
}
|
||||||
|
return m.nativeFirewall.AddOutputDNAT(localAddr, protocol, sourcePort, targetPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveOutputDNAT delegates to the native firewall if available.
|
||||||
|
func (m *Manager) RemoveOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error {
|
||||||
|
if m.nativeFirewall == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return m.nativeFirewall.RemoveOutputDNAT(localAddr, protocol, sourcePort, targetPort)
|
||||||
|
}
|
||||||
|
|
||||||
// translateInboundPortDNAT applies port-specific DNAT translation to inbound packets.
|
// translateInboundPortDNAT applies port-specific DNAT translation to inbound packets.
|
||||||
func (m *Manager) translateInboundPortDNAT(packetData []byte, d *decoder, srcIP, dstIP netip.Addr) bool {
|
func (m *Manager) translateInboundPortDNAT(packetData []byte, d *decoder, srcIP, dstIP netip.Addr) bool {
|
||||||
if !m.portDNATEnabled.Load() {
|
if !m.portDNATEnabled.Load() {
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ type PeerRule struct {
|
|||||||
protoLayer gopacket.LayerType
|
protoLayer gopacket.LayerType
|
||||||
sPort *firewall.Port
|
sPort *firewall.Port
|
||||||
dPort *firewall.Port
|
dPort *firewall.Port
|
||||||
drop bool
|
drop bool
|
||||||
|
|
||||||
udpHook func([]byte) bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ID returns the rule id
|
// ID returns the rule id
|
||||||
|
|||||||
@@ -399,21 +399,17 @@ func TestTracePacket(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "UDPTraffic_WithHook",
|
name: "UDPTraffic_WithHook",
|
||||||
setup: func(m *Manager) {
|
setup: func(m *Manager) {
|
||||||
hookFunc := func([]byte) bool {
|
m.SetUDPPacketHook(netip.MustParseAddr("100.10.255.254"), 53, func([]byte) bool {
|
||||||
return true
|
return true // drop (intercepted by hook)
|
||||||
}
|
})
|
||||||
m.AddUDPPacketHook(true, netip.MustParseAddr("1.1.1.1"), 53, hookFunc)
|
|
||||||
},
|
},
|
||||||
packetBuilder: func() *PacketBuilder {
|
packetBuilder: func() *PacketBuilder {
|
||||||
return createPacketBuilder("1.1.1.1", "100.10.0.100", "udp", 12345, 53, fw.RuleDirectionIN)
|
return createPacketBuilder("100.10.0.100", "100.10.255.254", "udp", 12345, 53, fw.RuleDirectionOUT)
|
||||||
},
|
},
|
||||||
expectedStages: []PacketStage{
|
expectedStages: []PacketStage{
|
||||||
StageReceived,
|
StageReceived,
|
||||||
StageInboundPortDNAT,
|
StageOutbound1to1NAT,
|
||||||
StageInbound1to1NAT,
|
StageOutboundPortReverse,
|
||||||
StageConntrack,
|
|
||||||
StageRouting,
|
|
||||||
StagePeerACL,
|
|
||||||
StageCompleted,
|
StageCompleted,
|
||||||
},
|
},
|
||||||
expectedAllow: false,
|
expectedAllow: false,
|
||||||
|
|||||||
@@ -15,14 +15,17 @@ type PacketFilter interface {
|
|||||||
// FilterInbound filter incoming packets from external sources to host
|
// FilterInbound filter incoming packets from external sources to host
|
||||||
FilterInbound(packetData []byte, size int) bool
|
FilterInbound(packetData []byte, size int) bool
|
||||||
|
|
||||||
// AddUDPPacketHook calls hook when UDP packet from given direction matched
|
// SetUDPPacketHook registers a hook for outbound UDP packets matching the given IP and port.
|
||||||
//
|
// Hook function returns true if the packet should be dropped.
|
||||||
// Hook function returns flag which indicates should be the matched package dropped or not.
|
// Only one UDP hook is supported; calling again replaces the previous hook.
|
||||||
// Hook function receives raw network packet data as argument.
|
// Pass nil hook to remove.
|
||||||
AddUDPPacketHook(in bool, ip netip.Addr, dPort uint16, hook func(packet []byte) bool) string
|
SetUDPPacketHook(ip netip.Addr, dPort uint16, hook func(packet []byte) bool)
|
||||||
|
|
||||||
// RemovePacketHook removes hook by ID
|
// SetTCPPacketHook registers a hook for outbound TCP packets matching the given IP and port.
|
||||||
RemovePacketHook(hookID string) error
|
// Hook function returns true if the packet should be dropped.
|
||||||
|
// Only one TCP hook is supported; calling again replaces the previous hook.
|
||||||
|
// Pass nil hook to remove.
|
||||||
|
SetTCPPacketHook(ip netip.Addr, dPort uint16, hook func(packet []byte) bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilteredDevice to override Read or Write of packets
|
// FilteredDevice to override Read or Write of packets
|
||||||
|
|||||||
@@ -34,18 +34,28 @@ func (m *MockPacketFilter) EXPECT() *MockPacketFilterMockRecorder {
|
|||||||
return m.recorder
|
return m.recorder
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddUDPPacketHook mocks base method.
|
// SetUDPPacketHook mocks base method.
|
||||||
func (m *MockPacketFilter) AddUDPPacketHook(arg0 bool, arg1 netip.Addr, arg2 uint16, arg3 func([]byte) bool) string {
|
func (m *MockPacketFilter) SetUDPPacketHook(arg0 netip.Addr, arg1 uint16, arg2 func([]byte) bool) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
ret := m.ctrl.Call(m, "AddUDPPacketHook", arg0, arg1, arg2, arg3)
|
m.ctrl.Call(m, "SetUDPPacketHook", arg0, arg1, arg2)
|
||||||
ret0, _ := ret[0].(string)
|
|
||||||
return ret0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddUDPPacketHook indicates an expected call of AddUDPPacketHook.
|
// SetUDPPacketHook indicates an expected call of SetUDPPacketHook.
|
||||||
func (mr *MockPacketFilterMockRecorder) AddUDPPacketHook(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
|
func (mr *MockPacketFilterMockRecorder) SetUDPPacketHook(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUDPPacketHook", reflect.TypeOf((*MockPacketFilter)(nil).AddUDPPacketHook), arg0, arg1, arg2, arg3)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetUDPPacketHook", reflect.TypeOf((*MockPacketFilter)(nil).SetUDPPacketHook), arg0, arg1, arg2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCPPacketHook mocks base method.
|
||||||
|
func (m *MockPacketFilter) SetTCPPacketHook(arg0 netip.Addr, arg1 uint16, arg2 func([]byte) bool) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
m.ctrl.Call(m, "SetTCPPacketHook", arg0, arg1, arg2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTCPPacketHook indicates an expected call of SetTCPPacketHook.
|
||||||
|
func (mr *MockPacketFilterMockRecorder) SetTCPPacketHook(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTCPPacketHook", reflect.TypeOf((*MockPacketFilter)(nil).SetTCPPacketHook), arg0, arg1, arg2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FilterInbound mocks base method.
|
// FilterInbound mocks base method.
|
||||||
@@ -75,17 +85,3 @@ func (mr *MockPacketFilterMockRecorder) FilterOutbound(arg0 interface{}, arg1 an
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterOutbound", reflect.TypeOf((*MockPacketFilter)(nil).FilterOutbound), arg0, arg1)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterOutbound", reflect.TypeOf((*MockPacketFilter)(nil).FilterOutbound), arg0, arg1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemovePacketHook mocks base method.
|
|
||||||
func (m *MockPacketFilter) RemovePacketHook(arg0 string) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "RemovePacketHook", arg0)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemovePacketHook indicates an expected call of RemovePacketHook.
|
|
||||||
func (mr *MockPacketFilterMockRecorder) RemovePacketHook(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemovePacketHook", reflect.TypeOf((*MockPacketFilter)(nil).RemovePacketHook), arg0)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
|
||||||
// Source: github.com/netbirdio/netbird/client/iface (interfaces: PacketFilter)
|
|
||||||
|
|
||||||
// Package mocks is a generated GoMock package.
|
|
||||||
package mocks
|
|
||||||
|
|
||||||
import (
|
|
||||||
net "net"
|
|
||||||
reflect "reflect"
|
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockPacketFilter is a mock of PacketFilter interface.
|
|
||||||
type MockPacketFilter struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockPacketFilterMockRecorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockPacketFilterMockRecorder is the mock recorder for MockPacketFilter.
|
|
||||||
type MockPacketFilterMockRecorder struct {
|
|
||||||
mock *MockPacketFilter
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockPacketFilter creates a new mock instance.
|
|
||||||
func NewMockPacketFilter(ctrl *gomock.Controller) *MockPacketFilter {
|
|
||||||
mock := &MockPacketFilter{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockPacketFilterMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
|
||||||
func (m *MockPacketFilter) EXPECT() *MockPacketFilterMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddUDPPacketHook mocks base method.
|
|
||||||
func (m *MockPacketFilter) AddUDPPacketHook(arg0 bool, arg1 net.IP, arg2 uint16, arg3 func(*net.UDPAddr, []byte) bool) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
m.ctrl.Call(m, "AddUDPPacketHook", arg0, arg1, arg2, arg3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddUDPPacketHook indicates an expected call of AddUDPPacketHook.
|
|
||||||
func (mr *MockPacketFilterMockRecorder) AddUDPPacketHook(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddUDPPacketHook", reflect.TypeOf((*MockPacketFilter)(nil).AddUDPPacketHook), arg0, arg1, arg2, arg3)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilterInbound mocks base method.
|
|
||||||
func (m *MockPacketFilter) FilterInbound(arg0 []byte) bool {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "FilterInbound", arg0)
|
|
||||||
ret0, _ := ret[0].(bool)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilterInbound indicates an expected call of FilterInbound.
|
|
||||||
func (mr *MockPacketFilterMockRecorder) FilterInbound(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterInbound", reflect.TypeOf((*MockPacketFilter)(nil).FilterInbound), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilterOutbound mocks base method.
|
|
||||||
func (m *MockPacketFilter) FilterOutbound(arg0 []byte) bool {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "FilterOutbound", arg0)
|
|
||||||
ret0, _ := ret[0].(bool)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilterOutbound indicates an expected call of FilterOutbound.
|
|
||||||
func (mr *MockPacketFilterMockRecorder) FilterOutbound(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FilterOutbound", reflect.TypeOf((*MockPacketFilter)(nil).FilterOutbound), arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNetwork mocks base method.
|
|
||||||
func (m *MockPacketFilter) SetNetwork(arg0 *net.IPNet) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
m.ctrl.Call(m, "SetNetwork", arg0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetNetwork indicates an expected call of SetNetwork.
|
|
||||||
func (mr *MockPacketFilterMockRecorder) SetNetwork(arg0 interface{}) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNetwork", reflect.TypeOf((*MockPacketFilter)(nil).SetNetwork), arg0)
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,9 @@ import (
|
|||||||
var flowLogger = netflow.NewManager(nil, []byte{}, nil).GetLogger()
|
var flowLogger = netflow.NewManager(nil, []byte{}, nil).GetLogger()
|
||||||
|
|
||||||
func TestDefaultManager(t *testing.T) {
|
func TestDefaultManager(t *testing.T) {
|
||||||
|
t.Setenv("NB_WG_KERNEL_DISABLED", "true")
|
||||||
|
t.Setenv(firewall.EnvForceUserspaceFirewall, "true")
|
||||||
|
|
||||||
networkMap := &mgmProto.NetworkMap{
|
networkMap := &mgmProto.NetworkMap{
|
||||||
FirewallRules: []*mgmProto.FirewallRule{
|
FirewallRules: []*mgmProto.FirewallRule{
|
||||||
{
|
{
|
||||||
@@ -135,6 +138,7 @@ func TestDefaultManager(t *testing.T) {
|
|||||||
func TestDefaultManagerStateless(t *testing.T) {
|
func TestDefaultManagerStateless(t *testing.T) {
|
||||||
// stateless currently only in userspace, so we have to disable kernel
|
// stateless currently only in userspace, so we have to disable kernel
|
||||||
t.Setenv("NB_WG_KERNEL_DISABLED", "true")
|
t.Setenv("NB_WG_KERNEL_DISABLED", "true")
|
||||||
|
t.Setenv(firewall.EnvForceUserspaceFirewall, "true")
|
||||||
t.Setenv("NB_DISABLE_CONNTRACK", "true")
|
t.Setenv("NB_DISABLE_CONNTRACK", "true")
|
||||||
|
|
||||||
networkMap := &mgmProto.NetworkMap{
|
networkMap := &mgmProto.NetworkMap{
|
||||||
@@ -194,6 +198,7 @@ func TestDefaultManagerStateless(t *testing.T) {
|
|||||||
// This tests the full ACL manager -> uspfilter integration.
|
// This tests the full ACL manager -> uspfilter integration.
|
||||||
func TestDenyRulesNotAccumulatedOnRepeatedApply(t *testing.T) {
|
func TestDenyRulesNotAccumulatedOnRepeatedApply(t *testing.T) {
|
||||||
t.Setenv("NB_WG_KERNEL_DISABLED", "true")
|
t.Setenv("NB_WG_KERNEL_DISABLED", "true")
|
||||||
|
t.Setenv(firewall.EnvForceUserspaceFirewall, "true")
|
||||||
|
|
||||||
networkMap := &mgmProto.NetworkMap{
|
networkMap := &mgmProto.NetworkMap{
|
||||||
FirewallRules: []*mgmProto.FirewallRule{
|
FirewallRules: []*mgmProto.FirewallRule{
|
||||||
@@ -258,6 +263,7 @@ func TestDenyRulesNotAccumulatedOnRepeatedApply(t *testing.T) {
|
|||||||
// up when they're removed from the network map in a subsequent update.
|
// up when they're removed from the network map in a subsequent update.
|
||||||
func TestDenyRulesCleanedUpOnRemoval(t *testing.T) {
|
func TestDenyRulesCleanedUpOnRemoval(t *testing.T) {
|
||||||
t.Setenv("NB_WG_KERNEL_DISABLED", "true")
|
t.Setenv("NB_WG_KERNEL_DISABLED", "true")
|
||||||
|
t.Setenv(firewall.EnvForceUserspaceFirewall, "true")
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
@@ -339,6 +345,7 @@ func TestDenyRulesCleanedUpOnRemoval(t *testing.T) {
|
|||||||
// one added without leaking.
|
// one added without leaking.
|
||||||
func TestRuleUpdateChangingAction(t *testing.T) {
|
func TestRuleUpdateChangingAction(t *testing.T) {
|
||||||
t.Setenv("NB_WG_KERNEL_DISABLED", "true")
|
t.Setenv("NB_WG_KERNEL_DISABLED", "true")
|
||||||
|
t.Setenv(firewall.EnvForceUserspaceFirewall, "true")
|
||||||
|
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ func (c *ConnectClient) RunOniOS(
|
|||||||
fileDescriptor int32,
|
fileDescriptor int32,
|
||||||
networkChangeListener listener.NetworkChangeListener,
|
networkChangeListener listener.NetworkChangeListener,
|
||||||
dnsManager dns.IosDnsManager,
|
dnsManager dns.IosDnsManager,
|
||||||
|
dnsAddresses []netip.AddrPort,
|
||||||
stateFilePath string,
|
stateFilePath string,
|
||||||
) error {
|
) error {
|
||||||
// Set GC percent to 5% to reduce memory usage as iOS only allows 50MB of memory for the extension.
|
// Set GC percent to 5% to reduce memory usage as iOS only allows 50MB of memory for the extension.
|
||||||
@@ -120,6 +121,7 @@ func (c *ConnectClient) RunOniOS(
|
|||||||
FileDescriptor: fileDescriptor,
|
FileDescriptor: fileDescriptor,
|
||||||
NetworkChangeListener: networkChangeListener,
|
NetworkChangeListener: networkChangeListener,
|
||||||
DnsManager: dnsManager,
|
DnsManager: dnsManager,
|
||||||
|
HostDNSAddresses: dnsAddresses,
|
||||||
StateFilePath: stateFilePath,
|
StateFilePath: stateFilePath,
|
||||||
}
|
}
|
||||||
return c.run(mobileDependency, nil, "")
|
return c.run(mobileDependency, nil, "")
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import (
|
|||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/anonymize"
|
"github.com/netbirdio/netbird/client/anonymize"
|
||||||
|
"github.com/netbirdio/netbird/client/configs"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
"github.com/netbirdio/netbird/client/internal/updater/installer"
|
"github.com/netbirdio/netbird/client/internal/updater/installer"
|
||||||
@@ -52,6 +53,7 @@ resolved_domains.txt: Anonymized resolved domain IP addresses from the status re
|
|||||||
config.txt: Anonymized configuration information of the NetBird client.
|
config.txt: Anonymized configuration information of the NetBird client.
|
||||||
network_map.json: Anonymized sync response containing peer configurations, routes, DNS settings, and firewall rules.
|
network_map.json: Anonymized sync response containing peer configurations, routes, DNS settings, and firewall rules.
|
||||||
state.json: Anonymized client state dump containing netbird states for the active profile.
|
state.json: Anonymized client state dump containing netbird states for the active profile.
|
||||||
|
service_params.json: Sanitized service install parameters (service.json). Sensitive environment variable values are masked. Only present when service.json exists.
|
||||||
metrics.txt: Buffered client metrics in InfluxDB line protocol format. Only present when metrics collection is enabled. Peer identifiers are anonymized.
|
metrics.txt: Buffered client metrics in InfluxDB line protocol format. Only present when metrics collection is enabled. Peer identifiers are anonymized.
|
||||||
mutex.prof: Mutex profiling information.
|
mutex.prof: Mutex profiling information.
|
||||||
goroutine.prof: Goroutine profiling information.
|
goroutine.prof: Goroutine profiling information.
|
||||||
@@ -359,6 +361,10 @@ func (g *BundleGenerator) createArchive() error {
|
|||||||
log.Errorf("failed to add corrupted state files to debug bundle: %v", err)
|
log.Errorf("failed to add corrupted state files to debug bundle: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := g.addServiceParams(); err != nil {
|
||||||
|
log.Errorf("failed to add service params to debug bundle: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := g.addMetrics(); err != nil {
|
if err := g.addMetrics(); err != nil {
|
||||||
log.Errorf("failed to add metrics to debug bundle: %v", err)
|
log.Errorf("failed to add metrics to debug bundle: %v", err)
|
||||||
}
|
}
|
||||||
@@ -488,6 +494,90 @@ func (g *BundleGenerator) addConfig() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
serviceParamsFile = "service.json"
|
||||||
|
serviceParamsBundle = "service_params.json"
|
||||||
|
maskedValue = "***"
|
||||||
|
envVarPrefix = "NB_"
|
||||||
|
jsonKeyManagementURL = "management_url"
|
||||||
|
jsonKeyServiceEnv = "service_env_vars"
|
||||||
|
)
|
||||||
|
|
||||||
|
var sensitiveEnvSubstrings = []string{"key", "token", "secret", "password", "credential"}
|
||||||
|
|
||||||
|
// addServiceParams reads the service.json file and adds a sanitized version to the bundle.
|
||||||
|
// Non-NB_ env vars and vars with sensitive names are masked. Other NB_ values are anonymized.
|
||||||
|
func (g *BundleGenerator) addServiceParams() error {
|
||||||
|
path := filepath.Join(configs.StateDir, serviceParamsFile)
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("read service params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var params map[string]any
|
||||||
|
if err := json.Unmarshal(data, ¶ms); err != nil {
|
||||||
|
return fmt.Errorf("parse service params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.anonymize {
|
||||||
|
if mgmtURL, ok := params[jsonKeyManagementURL].(string); ok && mgmtURL != "" {
|
||||||
|
params[jsonKeyManagementURL] = g.anonymizer.AnonymizeURI(mgmtURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g.sanitizeServiceEnvVars(params)
|
||||||
|
|
||||||
|
sanitizedData, err := json.MarshalIndent(params, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal sanitized service params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.addFileToZip(bytes.NewReader(sanitizedData), serviceParamsBundle); err != nil {
|
||||||
|
return fmt.Errorf("add service params to zip: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeServiceEnvVars masks or anonymizes env var values in service params.
|
||||||
|
// Non-NB_ vars and vars with sensitive names (key, token, etc.) are fully masked.
|
||||||
|
// Other NB_ var values are passed through the anonymizer when anonymization is enabled.
|
||||||
|
func (g *BundleGenerator) sanitizeServiceEnvVars(params map[string]any) {
|
||||||
|
envVars, ok := params[jsonKeyServiceEnv].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitized := make(map[string]any, len(envVars))
|
||||||
|
for k, v := range envVars {
|
||||||
|
val, _ := v.(string)
|
||||||
|
switch {
|
||||||
|
case !strings.HasPrefix(k, envVarPrefix) || isSensitiveEnvVar(k):
|
||||||
|
sanitized[k] = maskedValue
|
||||||
|
case g.anonymize:
|
||||||
|
sanitized[k] = g.anonymizer.AnonymizeString(val)
|
||||||
|
default:
|
||||||
|
sanitized[k] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
params[jsonKeyServiceEnv] = sanitized
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSensitiveEnvVar returns true for env var names that may contain secrets.
|
||||||
|
func isSensitiveEnvVar(key string) bool {
|
||||||
|
lower := strings.ToLower(key)
|
||||||
|
for _, s := range sensitiveEnvSubstrings {
|
||||||
|
if strings.Contains(lower, s) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder) {
|
func (g *BundleGenerator) addCommonConfigFields(configContent *strings.Builder) {
|
||||||
configContent.WriteString("NetBird Client Configuration:\n\n")
|
configContent.WriteString("NetBird Client Configuration:\n\n")
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package debug
|
package debug
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -10,6 +14,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/anonymize"
|
"github.com/netbirdio/netbird/client/anonymize"
|
||||||
|
"github.com/netbirdio/netbird/client/configs"
|
||||||
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -420,6 +425,226 @@ func TestAnonymizeNetworkMap(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsSensitiveEnvVar(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
key string
|
||||||
|
sensitive bool
|
||||||
|
}{
|
||||||
|
{"NB_SETUP_KEY", true},
|
||||||
|
{"NB_API_TOKEN", true},
|
||||||
|
{"NB_CLIENT_SECRET", true},
|
||||||
|
{"NB_PASSWORD", true},
|
||||||
|
{"NB_CREDENTIAL", true},
|
||||||
|
{"NB_LOG_LEVEL", false},
|
||||||
|
{"NB_MANAGEMENT_URL", false},
|
||||||
|
{"NB_HOSTNAME", false},
|
||||||
|
{"HOME", false},
|
||||||
|
{"PATH", false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.key, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.sensitive, isSensitiveEnvVar(tt.key))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSanitizeServiceEnvVars(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
anonymize bool
|
||||||
|
input map[string]any
|
||||||
|
check func(t *testing.T, params map[string]any)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no env vars key",
|
||||||
|
anonymize: false,
|
||||||
|
input: map[string]any{"management_url": "https://mgmt.example.com"},
|
||||||
|
check: func(t *testing.T, params map[string]any) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Equal(t, "https://mgmt.example.com", params["management_url"], "non-env fields should be untouched")
|
||||||
|
_, ok := params[jsonKeyServiceEnv]
|
||||||
|
assert.False(t, ok, "service_env_vars should not be added")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-NB vars are masked",
|
||||||
|
anonymize: false,
|
||||||
|
input: map[string]any{
|
||||||
|
jsonKeyServiceEnv: map[string]any{
|
||||||
|
"HOME": "/root",
|
||||||
|
"PATH": "/usr/bin",
|
||||||
|
"NB_LOG_LEVEL": "debug",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, params map[string]any) {
|
||||||
|
t.Helper()
|
||||||
|
env := params[jsonKeyServiceEnv].(map[string]any)
|
||||||
|
assert.Equal(t, maskedValue, env["HOME"], "non-NB_ var should be masked")
|
||||||
|
assert.Equal(t, maskedValue, env["PATH"], "non-NB_ var should be masked")
|
||||||
|
assert.Equal(t, "debug", env["NB_LOG_LEVEL"], "safe NB_ var should pass through")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sensitive NB vars are masked",
|
||||||
|
anonymize: false,
|
||||||
|
input: map[string]any{
|
||||||
|
jsonKeyServiceEnv: map[string]any{
|
||||||
|
"NB_SETUP_KEY": "abc123",
|
||||||
|
"NB_API_TOKEN": "tok_xyz",
|
||||||
|
"NB_LOG_LEVEL": "info",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, params map[string]any) {
|
||||||
|
t.Helper()
|
||||||
|
env := params[jsonKeyServiceEnv].(map[string]any)
|
||||||
|
assert.Equal(t, maskedValue, env["NB_SETUP_KEY"], "sensitive NB_ var should be masked")
|
||||||
|
assert.Equal(t, maskedValue, env["NB_API_TOKEN"], "sensitive NB_ var should be masked")
|
||||||
|
assert.Equal(t, "info", env["NB_LOG_LEVEL"], "safe NB_ var should pass through")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "safe NB vars anonymized when anonymize is true",
|
||||||
|
anonymize: true,
|
||||||
|
input: map[string]any{
|
||||||
|
jsonKeyServiceEnv: map[string]any{
|
||||||
|
"NB_MANAGEMENT_URL": "https://mgmt.example.com:443",
|
||||||
|
"NB_LOG_LEVEL": "debug",
|
||||||
|
"NB_SETUP_KEY": "secret",
|
||||||
|
"SOME_OTHER": "val",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
check: func(t *testing.T, params map[string]any) {
|
||||||
|
t.Helper()
|
||||||
|
env := params[jsonKeyServiceEnv].(map[string]any)
|
||||||
|
// Safe NB_ values should be anonymized (not the original, not masked)
|
||||||
|
mgmtVal := env["NB_MANAGEMENT_URL"].(string)
|
||||||
|
assert.NotEqual(t, "https://mgmt.example.com:443", mgmtVal, "should be anonymized")
|
||||||
|
assert.NotEqual(t, maskedValue, mgmtVal, "should not be masked")
|
||||||
|
|
||||||
|
logVal := env["NB_LOG_LEVEL"].(string)
|
||||||
|
assert.NotEqual(t, maskedValue, logVal, "safe NB_ var should not be masked")
|
||||||
|
|
||||||
|
// Sensitive and non-NB_ still masked
|
||||||
|
assert.Equal(t, maskedValue, env["NB_SETUP_KEY"])
|
||||||
|
assert.Equal(t, maskedValue, env["SOME_OTHER"])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
anonymizer := anonymize.NewAnonymizer(anonymize.DefaultAddresses())
|
||||||
|
g := &BundleGenerator{
|
||||||
|
anonymize: tt.anonymize,
|
||||||
|
anonymizer: anonymizer,
|
||||||
|
}
|
||||||
|
g.sanitizeServiceEnvVars(tt.input)
|
||||||
|
tt.check(t, tt.input)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddServiceParams(t *testing.T) {
|
||||||
|
t.Run("missing service.json returns nil", func(t *testing.T) {
|
||||||
|
g := &BundleGenerator{
|
||||||
|
anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()),
|
||||||
|
}
|
||||||
|
|
||||||
|
origStateDir := configs.StateDir
|
||||||
|
configs.StateDir = t.TempDir()
|
||||||
|
t.Cleanup(func() { configs.StateDir = origStateDir })
|
||||||
|
|
||||||
|
err := g.addServiceParams()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("management_url anonymized when anonymize is true", func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
origStateDir := configs.StateDir
|
||||||
|
configs.StateDir = dir
|
||||||
|
t.Cleanup(func() { configs.StateDir = origStateDir })
|
||||||
|
|
||||||
|
input := map[string]any{
|
||||||
|
jsonKeyManagementURL: "https://api.example.com:443",
|
||||||
|
jsonKeyServiceEnv: map[string]any{
|
||||||
|
"NB_LOG_LEVEL": "trace",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(input)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(dir, serviceParamsFile), data, 0600))
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
zw := zip.NewWriter(&buf)
|
||||||
|
|
||||||
|
g := &BundleGenerator{
|
||||||
|
anonymize: true,
|
||||||
|
anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()),
|
||||||
|
archive: zw,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, g.addServiceParams())
|
||||||
|
require.NoError(t, zw.Close())
|
||||||
|
|
||||||
|
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, zr.File, 1)
|
||||||
|
assert.Equal(t, serviceParamsBundle, zr.File[0].Name)
|
||||||
|
|
||||||
|
rc, err := zr.File[0].Open()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer rc.Close()
|
||||||
|
|
||||||
|
var result map[string]any
|
||||||
|
require.NoError(t, json.NewDecoder(rc).Decode(&result))
|
||||||
|
|
||||||
|
mgmt := result[jsonKeyManagementURL].(string)
|
||||||
|
assert.NotEqual(t, "https://api.example.com:443", mgmt, "management_url should be anonymized")
|
||||||
|
assert.NotEmpty(t, mgmt)
|
||||||
|
|
||||||
|
env := result[jsonKeyServiceEnv].(map[string]any)
|
||||||
|
assert.NotEqual(t, maskedValue, env["NB_LOG_LEVEL"], "safe NB_ var should not be masked")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("management_url preserved when anonymize is false", func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
origStateDir := configs.StateDir
|
||||||
|
configs.StateDir = dir
|
||||||
|
t.Cleanup(func() { configs.StateDir = origStateDir })
|
||||||
|
|
||||||
|
input := map[string]any{
|
||||||
|
jsonKeyManagementURL: "https://api.example.com:443",
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(input)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(dir, serviceParamsFile), data, 0600))
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
zw := zip.NewWriter(&buf)
|
||||||
|
|
||||||
|
g := &BundleGenerator{
|
||||||
|
anonymize: false,
|
||||||
|
anonymizer: anonymize.NewAnonymizer(anonymize.DefaultAddresses()),
|
||||||
|
archive: zw,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, g.addServiceParams())
|
||||||
|
require.NoError(t, zw.Close())
|
||||||
|
|
||||||
|
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rc, err := zr.File[0].Open()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer rc.Close()
|
||||||
|
|
||||||
|
var result map[string]any
|
||||||
|
require.NoError(t, json.NewDecoder(rc).Decode(&result))
|
||||||
|
|
||||||
|
assert.Equal(t, "https://api.example.com:443", result[jsonKeyManagementURL], "management_url should be preserved")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to check if IP is in CGNAT range
|
// Helper function to check if IP is in CGNAT range
|
||||||
func isInCGNATRange(ip net.IP) bool {
|
func isInCGNATRange(ip net.IP) bool {
|
||||||
cgnat := net.IPNet{
|
cgnat := net.IPNet{
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ func (w *ResponseWriterChain) WriteMsg(m *dns.Msg) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
w.response = m
|
w.response = m
|
||||||
|
if m.MsgHdr.Truncated {
|
||||||
|
w.SetMeta("truncated", "true")
|
||||||
|
}
|
||||||
return w.ResponseWriter.WriteMsg(m)
|
return w.ResponseWriter.WriteMsg(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,10 +198,14 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
requestID := resutil.GenerateRequestID()
|
requestID := resutil.GenerateRequestID()
|
||||||
logger := log.WithFields(log.Fields{
|
fields := log.Fields{
|
||||||
"request_id": requestID,
|
"request_id": requestID,
|
||||||
"dns_id": fmt.Sprintf("%04x", r.Id),
|
"dns_id": fmt.Sprintf("%04x", r.Id),
|
||||||
})
|
}
|
||||||
|
if addr := w.RemoteAddr(); addr != nil {
|
||||||
|
fields["client"] = addr.String()
|
||||||
|
}
|
||||||
|
logger := log.WithFields(fields)
|
||||||
|
|
||||||
question := r.Question[0]
|
question := r.Question[0]
|
||||||
qname := strings.ToLower(question.Name)
|
qname := strings.ToLower(question.Name)
|
||||||
@@ -261,9 +268,9 @@ func (c *HandlerChain) logResponse(logger *log.Entry, cw *ResponseWriterChain, q
|
|||||||
meta += " " + k + "=" + v
|
meta += " " + k + "=" + v
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Tracef("response: domain=%s rcode=%s answers=%s%s took=%s",
|
logger.Tracef("response: domain=%s rcode=%s answers=%s size=%dB%s took=%s",
|
||||||
qname, dns.RcodeToString[cw.response.Rcode], resutil.FormatAnswers(cw.response.Answer),
|
qname, dns.RcodeToString[cw.response.Rcode], resutil.FormatAnswers(cw.response.Answer),
|
||||||
meta, time.Since(startTime))
|
cw.response.Len(), meta, time.Since(startTime))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *HandlerChain) isHandlerMatch(qname string, entry HandlerEntry) bool {
|
func (c *HandlerChain) isHandlerMatch(qname string, entry HandlerEntry) bool {
|
||||||
|
|||||||
@@ -1263,9 +1263,9 @@ func TestLocalResolver_AuthoritativeFlag(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestLocalResolver_Stop tests cleanup on Stop
|
// TestLocalResolver_Stop tests cleanup on GracefullyStop
|
||||||
func TestLocalResolver_Stop(t *testing.T) {
|
func TestLocalResolver_Stop(t *testing.T) {
|
||||||
t.Run("Stop clears all state", func(t *testing.T) {
|
t.Run("GracefullyStop clears all state", func(t *testing.T) {
|
||||||
resolver := NewResolver()
|
resolver := NewResolver()
|
||||||
resolver.Update([]nbdns.CustomZone{{
|
resolver.Update([]nbdns.CustomZone{{
|
||||||
Domain: "example.com.",
|
Domain: "example.com.",
|
||||||
@@ -1285,7 +1285,7 @@ func TestLocalResolver_Stop(t *testing.T) {
|
|||||||
assert.False(t, resolver.isInManagedZone("host.example.com."))
|
assert.False(t, resolver.isInManagedZone("host.example.com."))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Stop is safe to call multiple times", func(t *testing.T) {
|
t.Run("GracefullyStop is safe to call multiple times", func(t *testing.T) {
|
||||||
resolver := NewResolver()
|
resolver := NewResolver()
|
||||||
resolver.Update([]nbdns.CustomZone{{
|
resolver.Update([]nbdns.CustomZone{{
|
||||||
Domain: "example.com.",
|
Domain: "example.com.",
|
||||||
@@ -1299,7 +1299,7 @@ func TestLocalResolver_Stop(t *testing.T) {
|
|||||||
resolver.Stop()
|
resolver.Stop()
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Stop cancels in-flight external resolution", func(t *testing.T) {
|
t.Run("GracefullyStop cancels in-flight external resolution", func(t *testing.T) {
|
||||||
resolver := NewResolver()
|
resolver := NewResolver()
|
||||||
|
|
||||||
lookupStarted := make(chan struct{})
|
lookupStarted := make(chan struct{})
|
||||||
|
|||||||
@@ -90,6 +90,11 @@ func (m *MockServer) SetRouteChecker(func(netip.Addr) bool) {
|
|||||||
// Mock implementation - no-op
|
// Mock implementation - no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetFirewall mock implementation of SetFirewall from Server interface
|
||||||
|
func (m *MockServer) SetFirewall(Firewall) {
|
||||||
|
// Mock implementation - no-op
|
||||||
|
}
|
||||||
|
|
||||||
// BeginBatch mock implementation of BeginBatch from Server interface
|
// BeginBatch mock implementation of BeginBatch from Server interface
|
||||||
func (m *MockServer) BeginBatch() {
|
func (m *MockServer) BeginBatch() {
|
||||||
// Mock implementation - no-op
|
// Mock implementation - no-op
|
||||||
|
|||||||
@@ -104,3 +104,23 @@ func (r *responseWriter) TsigTimersOnly(bool) {
|
|||||||
// After a call to Hijack(), the DNS package will not do anything with the connection.
|
// After a call to Hijack(), the DNS package will not do anything with the connection.
|
||||||
func (r *responseWriter) Hijack() {
|
func (r *responseWriter) Hijack() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remoteAddrFromPacket extracts the source IP:port from a decoded packet for logging.
|
||||||
|
func remoteAddrFromPacket(packet gopacket.Packet) *net.UDPAddr {
|
||||||
|
var srcIP net.IP
|
||||||
|
if ipv4 := packet.Layer(layers.LayerTypeIPv4); ipv4 != nil {
|
||||||
|
srcIP = ipv4.(*layers.IPv4).SrcIP
|
||||||
|
} else if ipv6 := packet.Layer(layers.LayerTypeIPv6); ipv6 != nil {
|
||||||
|
srcIP = ipv6.(*layers.IPv6).SrcIP
|
||||||
|
}
|
||||||
|
|
||||||
|
var srcPort int
|
||||||
|
if udp := packet.Layer(layers.LayerTypeUDP); udp != nil {
|
||||||
|
srcPort = int(udp.(*layers.UDP).SrcPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
if srcIP == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &net.UDPAddr{IP: srcIP, Port: srcPort}
|
||||||
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ type Server interface {
|
|||||||
UpdateServerConfig(domains dnsconfig.ServerDomains) error
|
UpdateServerConfig(domains dnsconfig.ServerDomains) error
|
||||||
PopulateManagementDomain(mgmtURL *url.URL) error
|
PopulateManagementDomain(mgmtURL *url.URL) error
|
||||||
SetRouteChecker(func(netip.Addr) bool)
|
SetRouteChecker(func(netip.Addr) bool)
|
||||||
|
SetFirewall(Firewall)
|
||||||
}
|
}
|
||||||
|
|
||||||
type nsGroupsByDomain struct {
|
type nsGroupsByDomain struct {
|
||||||
@@ -151,7 +152,7 @@ func NewDefaultServer(ctx context.Context, config DefaultServerConfig) (*Default
|
|||||||
if config.WgInterface.IsUserspaceBind() {
|
if config.WgInterface.IsUserspaceBind() {
|
||||||
dnsService = NewServiceViaMemory(config.WgInterface)
|
dnsService = NewServiceViaMemory(config.WgInterface)
|
||||||
} else {
|
} else {
|
||||||
dnsService = newServiceViaListener(config.WgInterface, addrPort)
|
dnsService = newServiceViaListener(config.WgInterface, addrPort, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
server := newDefaultServer(ctx, config.WgInterface, dnsService, config.StatusRecorder, config.StateManager, config.DisableSys)
|
server := newDefaultServer(ctx, config.WgInterface, dnsService, config.StatusRecorder, config.StateManager, config.DisableSys)
|
||||||
@@ -186,11 +187,16 @@ func NewDefaultServerIos(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
wgInterface WGIface,
|
wgInterface WGIface,
|
||||||
iosDnsManager IosDnsManager,
|
iosDnsManager IosDnsManager,
|
||||||
|
hostsDnsList []netip.AddrPort,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
disableSys bool,
|
disableSys bool,
|
||||||
) *DefaultServer {
|
) *DefaultServer {
|
||||||
|
log.Debugf("iOS host dns address list is: %v", hostsDnsList)
|
||||||
ds := newDefaultServer(ctx, wgInterface, NewServiceViaMemory(wgInterface), statusRecorder, nil, disableSys)
|
ds := newDefaultServer(ctx, wgInterface, NewServiceViaMemory(wgInterface), statusRecorder, nil, disableSys)
|
||||||
ds.iosDnsManager = iosDnsManager
|
ds.iosDnsManager = iosDnsManager
|
||||||
|
ds.hostsDNSHolder.set(hostsDnsList)
|
||||||
|
ds.permanent = true
|
||||||
|
ds.addHostRootZone()
|
||||||
return ds
|
return ds
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,6 +380,17 @@ func (s *DefaultServer) DnsIP() netip.Addr {
|
|||||||
return s.service.RuntimeIP()
|
return s.service.RuntimeIP()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetFirewall sets the firewall used for DNS port DNAT rules.
|
||||||
|
// This must be called before Initialize when using the listener-based service,
|
||||||
|
// because the firewall is typically not available at construction time.
|
||||||
|
func (s *DefaultServer) SetFirewall(fw Firewall) {
|
||||||
|
if svc, ok := s.service.(*serviceViaListener); ok {
|
||||||
|
svc.listenerFlagLock.Lock()
|
||||||
|
svc.firewall = fw
|
||||||
|
svc.listenerFlagLock.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Stop stops the server
|
// Stop stops the server
|
||||||
func (s *DefaultServer) Stop() {
|
func (s *DefaultServer) Stop() {
|
||||||
s.probeMu.Lock()
|
s.probeMu.Lock()
|
||||||
@@ -395,8 +412,12 @@ func (s *DefaultServer) Stop() {
|
|||||||
maps.Clear(s.extraDomains)
|
maps.Clear(s.extraDomains)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) disableDNS() error {
|
func (s *DefaultServer) disableDNS() (retErr error) {
|
||||||
defer s.service.Stop()
|
defer func() {
|
||||||
|
if err := s.service.Stop(); err != nil {
|
||||||
|
retErr = errors.Join(retErr, fmt.Errorf("stop DNS service: %w", err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if s.isUsingNoopHostManager() {
|
if s.isUsingNoopHostManager() {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -476,8 +476,8 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) {
|
|||||||
|
|
||||||
packetfilter := pfmock.NewMockPacketFilter(ctrl)
|
packetfilter := pfmock.NewMockPacketFilter(ctrl)
|
||||||
packetfilter.EXPECT().FilterOutbound(gomock.Any(), gomock.Any()).AnyTimes()
|
packetfilter.EXPECT().FilterOutbound(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
packetfilter.EXPECT().AddUDPPacketHook(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
|
packetfilter.EXPECT().SetUDPPacketHook(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
packetfilter.EXPECT().RemovePacketHook(gomock.Any())
|
packetfilter.EXPECT().SetTCPPacketHook(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
|
|
||||||
if err := wgIface.SetFilter(packetfilter); err != nil {
|
if err := wgIface.SetFilter(packetfilter); err != nil {
|
||||||
t.Errorf("set packet filter: %v", err)
|
t.Errorf("set packet filter: %v", err)
|
||||||
@@ -1071,7 +1071,7 @@ func (m *mockHandler) ID() types.HandlerID { return types.Hand
|
|||||||
type mockService struct{}
|
type mockService struct{}
|
||||||
|
|
||||||
func (m *mockService) Listen() error { return nil }
|
func (m *mockService) Listen() error { return nil }
|
||||||
func (m *mockService) Stop() {}
|
func (m *mockService) Stop() error { return nil }
|
||||||
func (m *mockService) RuntimeIP() netip.Addr { return netip.MustParseAddr("127.0.0.1") }
|
func (m *mockService) RuntimeIP() netip.Addr { return netip.MustParseAddr("127.0.0.1") }
|
||||||
func (m *mockService) RuntimePort() int { return 53 }
|
func (m *mockService) RuntimePort() int { return 53 }
|
||||||
func (m *mockService) RegisterMux(string, dns.Handler) {}
|
func (m *mockService) RegisterMux(string, dns.Handler) {}
|
||||||
|
|||||||
@@ -4,15 +4,25 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
|
|
||||||
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultPort = 53
|
DefaultPort = 53
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Firewall provides DNAT capabilities for DNS port redirection.
|
||||||
|
// This is used when the DNS server cannot bind port 53 directly
|
||||||
|
// and needs firewall rules to redirect traffic.
|
||||||
|
type Firewall interface {
|
||||||
|
AddOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error
|
||||||
|
RemoveOutputDNAT(localAddr netip.Addr, protocol firewall.Protocol, sourcePort, targetPort uint16) error
|
||||||
|
}
|
||||||
|
|
||||||
type service interface {
|
type service interface {
|
||||||
Listen() error
|
Listen() error
|
||||||
Stop()
|
Stop() error
|
||||||
RegisterMux(domain string, handler dns.Handler)
|
RegisterMux(domain string, handler dns.Handler)
|
||||||
DeregisterMux(key string)
|
DeregisterMux(key string)
|
||||||
RuntimePort() int
|
RuntimePort() int
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||||
|
|
||||||
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/internal/ebpf"
|
"github.com/netbirdio/netbird/client/internal/ebpf"
|
||||||
ebpfMgr "github.com/netbirdio/netbird/client/internal/ebpf/manager"
|
ebpfMgr "github.com/netbirdio/netbird/client/internal/ebpf/manager"
|
||||||
)
|
)
|
||||||
@@ -31,25 +35,33 @@ type serviceViaListener struct {
|
|||||||
dnsMux *dns.ServeMux
|
dnsMux *dns.ServeMux
|
||||||
customAddr *netip.AddrPort
|
customAddr *netip.AddrPort
|
||||||
server *dns.Server
|
server *dns.Server
|
||||||
|
tcpServer *dns.Server
|
||||||
listenIP netip.Addr
|
listenIP netip.Addr
|
||||||
listenPort uint16
|
listenPort uint16
|
||||||
listenerIsRunning bool
|
listenerIsRunning bool
|
||||||
listenerFlagLock sync.Mutex
|
listenerFlagLock sync.Mutex
|
||||||
ebpfService ebpfMgr.Manager
|
ebpfService ebpfMgr.Manager
|
||||||
|
firewall Firewall
|
||||||
|
tcpDNATConfigured bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServiceViaListener(wgIface WGIface, customAddr *netip.AddrPort) *serviceViaListener {
|
func newServiceViaListener(wgIface WGIface, customAddr *netip.AddrPort, fw Firewall) *serviceViaListener {
|
||||||
mux := dns.NewServeMux()
|
mux := dns.NewServeMux()
|
||||||
|
|
||||||
s := &serviceViaListener{
|
s := &serviceViaListener{
|
||||||
wgInterface: wgIface,
|
wgInterface: wgIface,
|
||||||
dnsMux: mux,
|
dnsMux: mux,
|
||||||
customAddr: customAddr,
|
customAddr: customAddr,
|
||||||
|
firewall: fw,
|
||||||
server: &dns.Server{
|
server: &dns.Server{
|
||||||
Net: "udp",
|
Net: "udp",
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
UDPSize: 65535,
|
UDPSize: 65535,
|
||||||
},
|
},
|
||||||
|
tcpServer: &dns.Server{
|
||||||
|
Net: "tcp",
|
||||||
|
Handler: mux,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
@@ -70,43 +82,86 @@ func (s *serviceViaListener) Listen() error {
|
|||||||
return fmt.Errorf("eval listen address: %w", err)
|
return fmt.Errorf("eval listen address: %w", err)
|
||||||
}
|
}
|
||||||
s.listenIP = s.listenIP.Unmap()
|
s.listenIP = s.listenIP.Unmap()
|
||||||
s.server.Addr = net.JoinHostPort(s.listenIP.String(), strconv.Itoa(int(s.listenPort)))
|
addr := net.JoinHostPort(s.listenIP.String(), strconv.Itoa(int(s.listenPort)))
|
||||||
log.Debugf("starting dns on %s", s.server.Addr)
|
s.server.Addr = addr
|
||||||
go func() {
|
s.tcpServer.Addr = addr
|
||||||
s.setListenerStatus(true)
|
|
||||||
defer s.setListenerStatus(false)
|
|
||||||
|
|
||||||
err := s.server.ListenAndServe()
|
log.Debugf("starting dns on %s (UDP + TCP)", addr)
|
||||||
if err != nil {
|
s.listenerIsRunning = true
|
||||||
log.Errorf("dns server running with %d port returned an error: %v. Will not retry", s.listenPort, err)
|
|
||||||
|
go func() {
|
||||||
|
if err := s.server.ListenAndServe(); err != nil {
|
||||||
|
log.Errorf("failed to run DNS UDP server on port %d: %v", s.listenPort, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.listenerFlagLock.Lock()
|
||||||
|
unexpected := s.listenerIsRunning
|
||||||
|
s.listenerIsRunning = false
|
||||||
|
s.listenerFlagLock.Unlock()
|
||||||
|
|
||||||
|
if unexpected {
|
||||||
|
if err := s.tcpServer.Shutdown(); err != nil {
|
||||||
|
log.Debugf("failed to shutdown DNS TCP server: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := s.tcpServer.ListenAndServe(); err != nil {
|
||||||
|
log.Errorf("failed to run DNS TCP server on port %d: %v", s.listenPort, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// When eBPF redirects UDP port 53 to our listen port, TCP still needs
|
||||||
|
// a DNAT rule because eBPF only handles UDP.
|
||||||
|
if s.ebpfService != nil && s.firewall != nil && s.listenPort != DefaultPort {
|
||||||
|
if err := s.firewall.AddOutputDNAT(s.listenIP, firewall.ProtocolTCP, DefaultPort, s.listenPort); err != nil {
|
||||||
|
log.Warnf("failed to add DNS TCP DNAT rule, TCP DNS on port 53 will not work: %v", err)
|
||||||
|
} else {
|
||||||
|
s.tcpDNATConfigured = true
|
||||||
|
log.Infof("added DNS TCP DNAT rule: %s:%d -> %s:%d", s.listenIP, DefaultPort, s.listenIP, s.listenPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serviceViaListener) Stop() {
|
func (s *serviceViaListener) Stop() error {
|
||||||
s.listenerFlagLock.Lock()
|
s.listenerFlagLock.Lock()
|
||||||
defer s.listenerFlagLock.Unlock()
|
defer s.listenerFlagLock.Unlock()
|
||||||
|
|
||||||
if !s.listenerIsRunning {
|
if !s.listenerIsRunning {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
s.listenerIsRunning = false
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
err := s.server.ShutdownContext(ctx)
|
var merr *multierror.Error
|
||||||
if err != nil {
|
|
||||||
log.Errorf("stopping dns server listener returned an error: %v", err)
|
if err := s.server.ShutdownContext(ctx); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("stop DNS UDP server: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.tcpServer.ShutdownContext(ctx); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("stop DNS TCP server: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.tcpDNATConfigured && s.firewall != nil {
|
||||||
|
if err := s.firewall.RemoveOutputDNAT(s.listenIP, firewall.ProtocolTCP, DefaultPort, s.listenPort); err != nil {
|
||||||
|
merr = multierror.Append(merr, fmt.Errorf("remove DNS TCP DNAT rule: %w", err))
|
||||||
|
}
|
||||||
|
s.tcpDNATConfigured = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.ebpfService != nil {
|
if s.ebpfService != nil {
|
||||||
err = s.ebpfService.FreeDNSFwd()
|
if err := s.ebpfService.FreeDNSFwd(); err != nil {
|
||||||
if err != nil {
|
merr = multierror.Append(merr, fmt.Errorf("stop traffic forwarder: %w", err))
|
||||||
log.Errorf("stopping traffic forwarder returned an error: %v", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nberrors.FormatErrorOrNil(merr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serviceViaListener) RegisterMux(pattern string, handler dns.Handler) {
|
func (s *serviceViaListener) RegisterMux(pattern string, handler dns.Handler) {
|
||||||
@@ -133,12 +188,6 @@ func (s *serviceViaListener) RuntimeIP() netip.Addr {
|
|||||||
return s.listenIP
|
return s.listenIP
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serviceViaListener) setListenerStatus(running bool) {
|
|
||||||
s.listenerFlagLock.Lock()
|
|
||||||
defer s.listenerFlagLock.Unlock()
|
|
||||||
|
|
||||||
s.listenerIsRunning = running
|
|
||||||
}
|
|
||||||
|
|
||||||
// evalListenAddress figure out the listen address for the DNS server
|
// evalListenAddress figure out the listen address for the DNS server
|
||||||
// first check the 53 port availability on WG interface or lo, if not success
|
// first check the 53 port availability on WG interface or lo, if not success
|
||||||
@@ -187,18 +236,28 @@ func (s *serviceViaListener) testFreePort(port int) (netip.Addr, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *serviceViaListener) tryToBind(ip netip.Addr, port int) bool {
|
func (s *serviceViaListener) tryToBind(ip netip.Addr, port int) bool {
|
||||||
addrString := net.JoinHostPort(ip.String(), strconv.Itoa(port))
|
addrPort := netip.AddrPortFrom(ip, uint16(port))
|
||||||
udpAddr := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(addrString))
|
|
||||||
probeListener, err := net.ListenUDP("udp", udpAddr)
|
udpAddr := net.UDPAddrFromAddrPort(addrPort)
|
||||||
|
udpLn, err := net.ListenUDP("udp", udpAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("binding dns on %s is not available, error: %s", addrString, err)
|
log.Warnf("binding dns UDP on %s is not available: %s", addrPort, err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if err := udpLn.Close(); err != nil {
|
||||||
err = probeListener.Close()
|
log.Debugf("close UDP probe listener: %s", err)
|
||||||
if err != nil {
|
|
||||||
log.Errorf("got an error closing the probe listener, error: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tcpAddr := net.TCPAddrFromAddrPort(addrPort)
|
||||||
|
tcpLn, err := net.ListenTCP("tcp", tcpAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("binding dns TCP on %s is not available: %s", addrPort, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if err := tcpLn.Close(); err != nil {
|
||||||
|
log.Debugf("close TCP probe listener: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
86
client/internal/dns/service_listener_test.go
Normal file
86
client/internal/dns/service_listener_test.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestServiceViaListener_TCPAndUDP(t *testing.T) {
|
||||||
|
handler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetReply(r)
|
||||||
|
m.Answer = append(m.Answer, &dns.A{
|
||||||
|
Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
|
||||||
|
A: net.ParseIP("192.0.2.1"),
|
||||||
|
})
|
||||||
|
if err := w.WriteMsg(m); err != nil {
|
||||||
|
t.Logf("write msg: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a service using a custom address to avoid needing root
|
||||||
|
svc := newServiceViaListener(nil, nil, nil)
|
||||||
|
svc.dnsMux.Handle(".", handler)
|
||||||
|
|
||||||
|
// Bind both transports up front to avoid TOCTOU races.
|
||||||
|
udpAddr := net.UDPAddrFromAddrPort(netip.AddrPortFrom(customIP, 0))
|
||||||
|
udpConn, err := net.ListenUDP("udp", udpAddr)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("cannot bind to 127.0.0.153, skipping")
|
||||||
|
}
|
||||||
|
port := uint16(udpConn.LocalAddr().(*net.UDPAddr).Port)
|
||||||
|
|
||||||
|
tcpAddr := net.TCPAddrFromAddrPort(netip.AddrPortFrom(customIP, port))
|
||||||
|
tcpLn, err := net.ListenTCP("tcp", tcpAddr)
|
||||||
|
if err != nil {
|
||||||
|
udpConn.Close()
|
||||||
|
t.Skip("cannot bind TCP on same port, skipping")
|
||||||
|
}
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("%s:%d", customIP, port)
|
||||||
|
svc.server.PacketConn = udpConn
|
||||||
|
svc.tcpServer.Listener = tcpLn
|
||||||
|
svc.listenIP = customIP
|
||||||
|
svc.listenPort = port
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := svc.server.ActivateAndServe(); err != nil {
|
||||||
|
t.Logf("udp server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
if err := svc.tcpServer.ActivateAndServe(); err != nil {
|
||||||
|
t.Logf("tcp server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
svc.listenerIsRunning = true
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
require.NoError(t, svc.Stop())
|
||||||
|
}()
|
||||||
|
|
||||||
|
q := new(dns.Msg).SetQuestion("example.com.", dns.TypeA)
|
||||||
|
|
||||||
|
// Test UDP query
|
||||||
|
udpClient := &dns.Client{Net: "udp", Timeout: 2 * time.Second}
|
||||||
|
udpResp, _, err := udpClient.Exchange(q, addr)
|
||||||
|
require.NoError(t, err, "UDP query should succeed")
|
||||||
|
require.NotNil(t, udpResp)
|
||||||
|
require.NotEmpty(t, udpResp.Answer)
|
||||||
|
assert.Contains(t, udpResp.Answer[0].String(), "192.0.2.1", "UDP response should contain expected IP")
|
||||||
|
|
||||||
|
// Test TCP query
|
||||||
|
tcpClient := &dns.Client{Net: "tcp", Timeout: 2 * time.Second}
|
||||||
|
tcpResp, _, err := tcpClient.Exchange(q, addr)
|
||||||
|
require.NoError(t, err, "TCP query should succeed")
|
||||||
|
require.NotNil(t, tcpResp)
|
||||||
|
require.NotEmpty(t, tcpResp.Answer)
|
||||||
|
assert.Contains(t, tcpResp.Answer[0].String(), "192.0.2.1", "TCP response should contain expected IP")
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -10,6 +11,7 @@ import (
|
|||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
nbnet "github.com/netbirdio/netbird/client/net"
|
nbnet "github.com/netbirdio/netbird/client/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,7 +20,8 @@ type ServiceViaMemory struct {
|
|||||||
dnsMux *dns.ServeMux
|
dnsMux *dns.ServeMux
|
||||||
runtimeIP netip.Addr
|
runtimeIP netip.Addr
|
||||||
runtimePort int
|
runtimePort int
|
||||||
udpFilterHookID string
|
tcpDNS *tcpDNSServer
|
||||||
|
tcpHookSet bool
|
||||||
listenerIsRunning bool
|
listenerIsRunning bool
|
||||||
listenerFlagLock sync.Mutex
|
listenerFlagLock sync.Mutex
|
||||||
}
|
}
|
||||||
@@ -28,14 +31,13 @@ func NewServiceViaMemory(wgIface WGIface) *ServiceViaMemory {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("get last ip from network: %v", err)
|
log.Errorf("get last ip from network: %v", err)
|
||||||
}
|
}
|
||||||
s := &ServiceViaMemory{
|
|
||||||
|
return &ServiceViaMemory{
|
||||||
wgInterface: wgIface,
|
wgInterface: wgIface,
|
||||||
dnsMux: dns.NewServeMux(),
|
dnsMux: dns.NewServeMux(),
|
||||||
|
|
||||||
runtimeIP: lastIP,
|
runtimeIP: lastIP,
|
||||||
runtimePort: DefaultPort,
|
runtimePort: DefaultPort,
|
||||||
}
|
}
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServiceViaMemory) Listen() error {
|
func (s *ServiceViaMemory) Listen() error {
|
||||||
@@ -46,10 +48,8 @@ func (s *ServiceViaMemory) Listen() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
if err := s.filterDNSTraffic(); err != nil {
|
||||||
s.udpFilterHookID, err = s.filterDNSTraffic()
|
return fmt.Errorf("filter dns traffic: %w", err)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("filter dns traffice: %w", err)
|
|
||||||
}
|
}
|
||||||
s.listenerIsRunning = true
|
s.listenerIsRunning = true
|
||||||
|
|
||||||
@@ -57,19 +57,29 @@ func (s *ServiceViaMemory) Listen() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServiceViaMemory) Stop() {
|
func (s *ServiceViaMemory) Stop() error {
|
||||||
s.listenerFlagLock.Lock()
|
s.listenerFlagLock.Lock()
|
||||||
defer s.listenerFlagLock.Unlock()
|
defer s.listenerFlagLock.Unlock()
|
||||||
|
|
||||||
if !s.listenerIsRunning {
|
if !s.listenerIsRunning {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.wgInterface.GetFilter().RemovePacketHook(s.udpFilterHookID); err != nil {
|
filter := s.wgInterface.GetFilter()
|
||||||
log.Errorf("unable to remove DNS packet hook: %s", err)
|
if filter != nil {
|
||||||
|
filter.SetUDPPacketHook(s.runtimeIP, uint16(s.runtimePort), nil)
|
||||||
|
if s.tcpHookSet {
|
||||||
|
filter.SetTCPPacketHook(s.runtimeIP, uint16(s.runtimePort), nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.tcpDNS != nil {
|
||||||
|
s.tcpDNS.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
s.listenerIsRunning = false
|
s.listenerIsRunning = false
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServiceViaMemory) RegisterMux(pattern string, handler dns.Handler) {
|
func (s *ServiceViaMemory) RegisterMux(pattern string, handler dns.Handler) {
|
||||||
@@ -88,10 +98,18 @@ func (s *ServiceViaMemory) RuntimeIP() netip.Addr {
|
|||||||
return s.runtimeIP
|
return s.runtimeIP
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServiceViaMemory) filterDNSTraffic() (string, error) {
|
func (s *ServiceViaMemory) filterDNSTraffic() error {
|
||||||
filter := s.wgInterface.GetFilter()
|
filter := s.wgInterface.GetFilter()
|
||||||
if filter == nil {
|
if filter == nil {
|
||||||
return "", fmt.Errorf("can't set DNS filter, filter not initialized")
|
return errors.New("DNS filter not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create TCP DNS server lazily here since the device may not exist at construction time.
|
||||||
|
if s.tcpDNS == nil {
|
||||||
|
if dev := s.wgInterface.GetDevice(); dev != nil {
|
||||||
|
// MTU only affects TCP segment sizing; DNS messages are small so this has no practical impact.
|
||||||
|
s.tcpDNS = newTCPDNSServer(s.dnsMux, dev.Device, s.runtimeIP, uint16(s.runtimePort), iface.DefaultMTU)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
firstLayerDecoder := layers.LayerTypeIPv4
|
firstLayerDecoder := layers.LayerTypeIPv4
|
||||||
@@ -100,12 +118,16 @@ func (s *ServiceViaMemory) filterDNSTraffic() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
hook := func(packetData []byte) bool {
|
hook := func(packetData []byte) bool {
|
||||||
// Decode the packet
|
|
||||||
packet := gopacket.NewPacket(packetData, firstLayerDecoder, gopacket.Default)
|
packet := gopacket.NewPacket(packetData, firstLayerDecoder, gopacket.Default)
|
||||||
|
|
||||||
// Get the UDP layer
|
|
||||||
udpLayer := packet.Layer(layers.LayerTypeUDP)
|
udpLayer := packet.Layer(layers.LayerTypeUDP)
|
||||||
udp := udpLayer.(*layers.UDP)
|
if udpLayer == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
udp, ok := udpLayer.(*layers.UDP)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
msg := new(dns.Msg)
|
msg := new(dns.Msg)
|
||||||
if err := msg.Unpack(udp.Payload); err != nil {
|
if err := msg.Unpack(udp.Payload); err != nil {
|
||||||
@@ -113,13 +135,30 @@ func (s *ServiceViaMemory) filterDNSTraffic() (string, error) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
writer := responseWriter{
|
dev := s.wgInterface.GetDevice()
|
||||||
packet: packet,
|
if dev == nil {
|
||||||
device: s.wgInterface.GetDevice().Device,
|
return true
|
||||||
}
|
}
|
||||||
go s.dnsMux.ServeDNS(&writer, msg)
|
|
||||||
|
writer := &responseWriter{
|
||||||
|
remote: remoteAddrFromPacket(packet),
|
||||||
|
packet: packet,
|
||||||
|
device: dev.Device,
|
||||||
|
}
|
||||||
|
go s.dnsMux.ServeDNS(writer, msg)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return filter.AddUDPPacketHook(false, s.runtimeIP, uint16(s.runtimePort), hook), nil
|
filter.SetUDPPacketHook(s.runtimeIP, uint16(s.runtimePort), hook)
|
||||||
|
|
||||||
|
if s.tcpDNS != nil {
|
||||||
|
tcpHook := func(packetData []byte) bool {
|
||||||
|
s.tcpDNS.InjectPacket(packetData)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
filter.SetTCPPacketHook(s.runtimeIP, uint16(s.runtimePort), tcpHook)
|
||||||
|
s.tcpHookSet = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
444
client/internal/dns/tcpstack.go
Normal file
444
client/internal/dns/tcpstack.go
Normal file
@@ -0,0 +1,444 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.zx2c4.com/wireguard/tun"
|
||||||
|
"gvisor.dev/gvisor/pkg/buffer"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
|
||||||
|
"gvisor.dev/gvisor/pkg/waiter"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dnsTCPReceiveWindow = 8192
|
||||||
|
dnsTCPMaxInFlight = 16
|
||||||
|
dnsTCPIdleTimeout = 30 * time.Second
|
||||||
|
dnsTCPReadTimeout = 5 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// tcpDNSServer is an on-demand TCP DNS server backed by a minimal gvisor stack.
|
||||||
|
// It is started lazily when a truncated DNS response is detected and shuts down
|
||||||
|
// after a period of inactivity to conserve resources.
|
||||||
|
type tcpDNSServer struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
s *stack.Stack
|
||||||
|
ep *dnsEndpoint
|
||||||
|
mux *dns.ServeMux
|
||||||
|
tunDev tun.Device
|
||||||
|
ip netip.Addr
|
||||||
|
port uint16
|
||||||
|
mtu uint16
|
||||||
|
|
||||||
|
running bool
|
||||||
|
closed bool
|
||||||
|
timerID uint64
|
||||||
|
timer *time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTCPDNSServer(mux *dns.ServeMux, tunDev tun.Device, ip netip.Addr, port uint16, mtu uint16) *tcpDNSServer {
|
||||||
|
return &tcpDNSServer{
|
||||||
|
mux: mux,
|
||||||
|
tunDev: tunDev,
|
||||||
|
ip: ip,
|
||||||
|
port: port,
|
||||||
|
mtu: mtu,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InjectPacket ensures the stack is running and delivers a raw IP packet into
|
||||||
|
// the gvisor stack for TCP processing. Combining both operations under a single
|
||||||
|
// lock prevents a race where the idle timer could stop the stack between
|
||||||
|
// start and delivery.
|
||||||
|
func (t *tcpDNSServer) InjectPacket(payload []byte) {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
if t.closed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !t.running {
|
||||||
|
if err := t.startLocked(); err != nil {
|
||||||
|
log.Errorf("failed to start TCP DNS stack: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.running = true
|
||||||
|
log.Debugf("TCP DNS stack started on %s:%d (triggered by %s)", t.ip, t.port, srcAddrFromPacket(payload))
|
||||||
|
}
|
||||||
|
t.resetTimerLocked()
|
||||||
|
|
||||||
|
ep := t.ep
|
||||||
|
if ep == nil || ep.dispatcher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
|
||||||
|
Payload: buffer.MakeWithData(payload),
|
||||||
|
})
|
||||||
|
// DeliverNetworkPacket takes ownership of the packet buffer; do not DecRef.
|
||||||
|
ep.dispatcher.DeliverNetworkPacket(ipv4.ProtocolNumber, pkt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop tears down the gvisor stack and releases resources permanently.
|
||||||
|
// After Stop, InjectPacket becomes a no-op.
|
||||||
|
func (t *tcpDNSServer) Stop() {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
t.stopLocked()
|
||||||
|
t.closed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tcpDNSServer) startLocked() error {
|
||||||
|
// TODO: add ipv6.NewProtocol when IPv6 overlay support lands.
|
||||||
|
s := stack.New(stack.Options{
|
||||||
|
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol},
|
||||||
|
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol},
|
||||||
|
HandleLocal: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
nicID := tcpip.NICID(1)
|
||||||
|
ep := &dnsEndpoint{
|
||||||
|
tunDev: t.tunDev,
|
||||||
|
}
|
||||||
|
ep.mtu.Store(uint32(t.mtu))
|
||||||
|
|
||||||
|
if err := s.CreateNIC(nicID, ep); err != nil {
|
||||||
|
s.Close()
|
||||||
|
s.Wait()
|
||||||
|
return fmt.Errorf("create NIC: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
protoAddr := tcpip.ProtocolAddress{
|
||||||
|
Protocol: ipv4.ProtocolNumber,
|
||||||
|
AddressWithPrefix: tcpip.AddressWithPrefix{
|
||||||
|
Address: tcpip.AddrFromSlice(t.ip.AsSlice()),
|
||||||
|
PrefixLen: 32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := s.AddProtocolAddress(nicID, protoAddr, stack.AddressProperties{}); err != nil {
|
||||||
|
s.Close()
|
||||||
|
s.Wait()
|
||||||
|
return fmt.Errorf("add protocol address: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.SetPromiscuousMode(nicID, true); err != nil {
|
||||||
|
s.Close()
|
||||||
|
s.Wait()
|
||||||
|
return fmt.Errorf("set promiscuous mode: %s", err)
|
||||||
|
}
|
||||||
|
if err := s.SetSpoofing(nicID, true); err != nil {
|
||||||
|
s.Close()
|
||||||
|
s.Wait()
|
||||||
|
return fmt.Errorf("set spoofing: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultSubnet, err := tcpip.NewSubnet(
|
||||||
|
tcpip.AddrFrom4([4]byte{0, 0, 0, 0}),
|
||||||
|
tcpip.MaskFromBytes([]byte{0, 0, 0, 0}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
s.Close()
|
||||||
|
s.Wait()
|
||||||
|
return fmt.Errorf("create default subnet: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.SetRouteTable([]tcpip.Route{
|
||||||
|
{Destination: defaultSubnet, NIC: nicID},
|
||||||
|
})
|
||||||
|
|
||||||
|
tcpFwd := tcp.NewForwarder(s, dnsTCPReceiveWindow, dnsTCPMaxInFlight, func(r *tcp.ForwarderRequest) {
|
||||||
|
t.handleTCPDNS(r)
|
||||||
|
})
|
||||||
|
s.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpFwd.HandlePacket)
|
||||||
|
|
||||||
|
t.s = s
|
||||||
|
t.ep = ep
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tcpDNSServer) stopLocked() {
|
||||||
|
if !t.running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.timer != nil {
|
||||||
|
t.timer.Stop()
|
||||||
|
t.timer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.s != nil {
|
||||||
|
t.s.Close()
|
||||||
|
t.s.Wait()
|
||||||
|
t.s = nil
|
||||||
|
}
|
||||||
|
t.ep = nil
|
||||||
|
t.running = false
|
||||||
|
|
||||||
|
log.Debugf("TCP DNS stack stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tcpDNSServer) resetTimerLocked() {
|
||||||
|
if t.timer != nil {
|
||||||
|
t.timer.Stop()
|
||||||
|
}
|
||||||
|
t.timerID++
|
||||||
|
id := t.timerID
|
||||||
|
t.timer = time.AfterFunc(dnsTCPIdleTimeout, func() {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
// Only stop if this timer is still the active one.
|
||||||
|
// A racing InjectPacket may have replaced it.
|
||||||
|
if t.timerID != id {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.stopLocked()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tcpDNSServer) handleTCPDNS(r *tcp.ForwarderRequest) {
|
||||||
|
id := r.ID()
|
||||||
|
|
||||||
|
wq := waiter.Queue{}
|
||||||
|
ep, epErr := r.CreateEndpoint(&wq)
|
||||||
|
if epErr != nil {
|
||||||
|
log.Debugf("TCP DNS: failed to create endpoint: %v", epErr)
|
||||||
|
r.Complete(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.Complete(false)
|
||||||
|
|
||||||
|
conn := gonet.NewTCPConn(&wq, ep)
|
||||||
|
defer func() {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
log.Tracef("TCP DNS: close conn: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Reset idle timer on activity
|
||||||
|
t.mu.Lock()
|
||||||
|
t.resetTimerLocked()
|
||||||
|
t.mu.Unlock()
|
||||||
|
|
||||||
|
localAddr := &net.TCPAddr{
|
||||||
|
IP: id.LocalAddress.AsSlice(),
|
||||||
|
Port: int(id.LocalPort),
|
||||||
|
}
|
||||||
|
remoteAddr := &net.TCPAddr{
|
||||||
|
IP: id.RemoteAddress.AsSlice(),
|
||||||
|
Port: int(id.RemotePort),
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if err := conn.SetReadDeadline(time.Now().Add(dnsTCPReadTimeout)); err != nil {
|
||||||
|
log.Debugf("TCP DNS: set deadline for %s: %v", remoteAddr, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := readTCPDNSMessage(conn)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
log.Debugf("TCP DNS: read from %s: %v", remoteAddr, err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
writer := &tcpResponseWriter{
|
||||||
|
conn: conn,
|
||||||
|
localAddr: localAddr,
|
||||||
|
remoteAddr: remoteAddr,
|
||||||
|
}
|
||||||
|
t.mux.ServeDNS(writer, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsEndpoint implements stack.LinkEndpoint for writing packets back via the tun device.
|
||||||
|
type dnsEndpoint struct {
|
||||||
|
dispatcher stack.NetworkDispatcher
|
||||||
|
tunDev tun.Device
|
||||||
|
mtu atomic.Uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *dnsEndpoint) Attach(dispatcher stack.NetworkDispatcher) { e.dispatcher = dispatcher }
|
||||||
|
func (e *dnsEndpoint) IsAttached() bool { return e.dispatcher != nil }
|
||||||
|
func (e *dnsEndpoint) MTU() uint32 { return e.mtu.Load() }
|
||||||
|
func (e *dnsEndpoint) Capabilities() stack.LinkEndpointCapabilities { return stack.CapabilityNone }
|
||||||
|
func (e *dnsEndpoint) MaxHeaderLength() uint16 { return 0 }
|
||||||
|
func (e *dnsEndpoint) LinkAddress() tcpip.LinkAddress { return "" }
|
||||||
|
func (e *dnsEndpoint) Wait() { /* no async work */ }
|
||||||
|
func (e *dnsEndpoint) ARPHardwareType() header.ARPHardwareType { return header.ARPHardwareNone }
|
||||||
|
func (e *dnsEndpoint) AddHeader(*stack.PacketBuffer) { /* IP-level endpoint, no link header */ }
|
||||||
|
func (e *dnsEndpoint) ParseHeader(*stack.PacketBuffer) bool { return true }
|
||||||
|
func (e *dnsEndpoint) Close() { /* lifecycle managed by tcpDNSServer */ }
|
||||||
|
func (e *dnsEndpoint) SetLinkAddress(tcpip.LinkAddress) { /* no link address for tun */ }
|
||||||
|
func (e *dnsEndpoint) SetMTU(mtu uint32) { e.mtu.Store(mtu) }
|
||||||
|
func (e *dnsEndpoint) SetOnCloseAction(func()) { /* not needed */ }
|
||||||
|
|
||||||
|
const tunPacketOffset = 40
|
||||||
|
|
||||||
|
func (e *dnsEndpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error) {
|
||||||
|
var written int
|
||||||
|
for _, pkt := range pkts.AsSlice() {
|
||||||
|
data := stack.PayloadSince(pkt.NetworkHeader())
|
||||||
|
if data == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
raw := data.AsSlice()
|
||||||
|
buf := make([]byte, tunPacketOffset, tunPacketOffset+len(raw))
|
||||||
|
buf = append(buf, raw...)
|
||||||
|
data.Release()
|
||||||
|
|
||||||
|
if _, err := e.tunDev.Write([][]byte{buf}, tunPacketOffset); err != nil {
|
||||||
|
log.Tracef("TCP DNS endpoint: failed to write packet: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
written++
|
||||||
|
}
|
||||||
|
return written, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tcpResponseWriter implements dns.ResponseWriter for TCP DNS connections.
|
||||||
|
type tcpResponseWriter struct {
|
||||||
|
conn *gonet.TCPConn
|
||||||
|
localAddr net.Addr
|
||||||
|
remoteAddr net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *tcpResponseWriter) LocalAddr() net.Addr {
|
||||||
|
return w.localAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *tcpResponseWriter) RemoteAddr() net.Addr {
|
||||||
|
return w.remoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *tcpResponseWriter) WriteMsg(msg *dns.Msg) error {
|
||||||
|
data, err := msg.Pack()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pack: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS TCP: 2-byte length prefix + message
|
||||||
|
buf := make([]byte, 2+len(data))
|
||||||
|
buf[0] = byte(len(data) >> 8)
|
||||||
|
buf[1] = byte(len(data))
|
||||||
|
copy(buf[2:], data)
|
||||||
|
|
||||||
|
if _, err = w.conn.Write(buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *tcpResponseWriter) Write(data []byte) (int, error) {
|
||||||
|
buf := make([]byte, 2+len(data))
|
||||||
|
buf[0] = byte(len(data) >> 8)
|
||||||
|
buf[1] = byte(len(data))
|
||||||
|
copy(buf[2:], data)
|
||||||
|
if _, err := w.conn.Write(buf); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *tcpResponseWriter) Close() error {
|
||||||
|
return w.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *tcpResponseWriter) TsigStatus() error { return nil }
|
||||||
|
func (w *tcpResponseWriter) TsigTimersOnly(bool) { /* TSIG not supported */ }
|
||||||
|
func (w *tcpResponseWriter) Hijack() { /* not supported */ }
|
||||||
|
|
||||||
|
// readTCPDNSMessage reads a single DNS message from a TCP connection (length-prefixed).
|
||||||
|
func readTCPDNSMessage(conn *gonet.TCPConn) (*dns.Msg, error) {
|
||||||
|
// DNS over TCP uses a 2-byte length prefix
|
||||||
|
lenBuf := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(conn, lenBuf); err != nil {
|
||||||
|
return nil, fmt.Errorf("read length: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgLen := int(lenBuf[0])<<8 | int(lenBuf[1])
|
||||||
|
if msgLen == 0 || msgLen > 65535 {
|
||||||
|
return nil, fmt.Errorf("invalid message length: %d", msgLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBuf := make([]byte, msgLen)
|
||||||
|
if _, err := io.ReadFull(conn, msgBuf); err != nil {
|
||||||
|
return nil, fmt.Errorf("read message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := new(dns.Msg)
|
||||||
|
if err := msg.Unpack(msgBuf); err != nil {
|
||||||
|
return nil, fmt.Errorf("unpack: %w", err)
|
||||||
|
}
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// srcAddrFromPacket extracts the source IP:port from a raw IP+TCP packet for logging.
|
||||||
|
// Supports both IPv4 and IPv6.
|
||||||
|
func srcAddrFromPacket(pkt []byte) netip.AddrPort {
|
||||||
|
if len(pkt) == 0 {
|
||||||
|
return netip.AddrPort{}
|
||||||
|
}
|
||||||
|
|
||||||
|
srcIP, transportOffset := srcIPFromPacket(pkt)
|
||||||
|
if !srcIP.IsValid() || len(pkt) < transportOffset+2 {
|
||||||
|
return netip.AddrPort{}
|
||||||
|
}
|
||||||
|
|
||||||
|
srcPort := uint16(pkt[transportOffset])<<8 | uint16(pkt[transportOffset+1])
|
||||||
|
return netip.AddrPortFrom(srcIP.Unmap(), srcPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func srcIPFromPacket(pkt []byte) (netip.Addr, int) {
|
||||||
|
switch header.IPVersion(pkt) {
|
||||||
|
case 4:
|
||||||
|
return srcIPv4(pkt)
|
||||||
|
case 6:
|
||||||
|
return srcIPv6(pkt)
|
||||||
|
default:
|
||||||
|
return netip.Addr{}, 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func srcIPv4(pkt []byte) (netip.Addr, int) {
|
||||||
|
if len(pkt) < header.IPv4MinimumSize {
|
||||||
|
return netip.Addr{}, 0
|
||||||
|
}
|
||||||
|
hdr := header.IPv4(pkt)
|
||||||
|
src := hdr.SourceAddress()
|
||||||
|
ip, ok := netip.AddrFromSlice(src.AsSlice())
|
||||||
|
if !ok {
|
||||||
|
return netip.Addr{}, 0
|
||||||
|
}
|
||||||
|
return ip, int(hdr.HeaderLength())
|
||||||
|
}
|
||||||
|
|
||||||
|
func srcIPv6(pkt []byte) (netip.Addr, int) {
|
||||||
|
if len(pkt) < header.IPv6MinimumSize {
|
||||||
|
return netip.Addr{}, 0
|
||||||
|
}
|
||||||
|
hdr := header.IPv6(pkt)
|
||||||
|
src := hdr.SourceAddress()
|
||||||
|
ip, ok := netip.AddrFromSlice(src.AsSlice())
|
||||||
|
if !ok {
|
||||||
|
return netip.Addr{}, 0
|
||||||
|
}
|
||||||
|
return ip, header.IPv6MinimumSize
|
||||||
|
}
|
||||||
@@ -41,10 +41,61 @@ const (
|
|||||||
|
|
||||||
reactivatePeriod = 30 * time.Second
|
reactivatePeriod = 30 * time.Second
|
||||||
probeTimeout = 2 * time.Second
|
probeTimeout = 2 * time.Second
|
||||||
|
|
||||||
|
// ipv6HeaderSize + udpHeaderSize, used to derive the maximum DNS UDP
|
||||||
|
// payload from the tunnel MTU.
|
||||||
|
ipUDPHeaderSize = 60 + 8
|
||||||
)
|
)
|
||||||
|
|
||||||
const testRecord = "com."
|
const testRecord = "com."
|
||||||
|
|
||||||
|
const (
|
||||||
|
protoUDP = "udp"
|
||||||
|
protoTCP = "tcp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dnsProtocolKey struct{}
|
||||||
|
|
||||||
|
// contextWithDNSProtocol stores the inbound DNS protocol ("udp" or "tcp") in context.
|
||||||
|
func contextWithDNSProtocol(ctx context.Context, network string) context.Context {
|
||||||
|
return context.WithValue(ctx, dnsProtocolKey{}, network)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsProtocolFromContext retrieves the inbound DNS protocol from context.
|
||||||
|
func dnsProtocolFromContext(ctx context.Context) string {
|
||||||
|
if ctx == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if v, ok := ctx.Value(dnsProtocolKey{}).(string); ok {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type upstreamProtocolKey struct{}
|
||||||
|
|
||||||
|
// upstreamProtocolResult holds the protocol used for the upstream exchange.
|
||||||
|
// Stored as a pointer in context so the exchange function can set it.
|
||||||
|
type upstreamProtocolResult struct {
|
||||||
|
protocol string
|
||||||
|
}
|
||||||
|
|
||||||
|
// contextWithupstreamProtocolResult stores a mutable result holder in the context.
|
||||||
|
func contextWithupstreamProtocolResult(ctx context.Context) (context.Context, *upstreamProtocolResult) {
|
||||||
|
r := &upstreamProtocolResult{}
|
||||||
|
return context.WithValue(ctx, upstreamProtocolKey{}, r), r
|
||||||
|
}
|
||||||
|
|
||||||
|
// setUpstreamProtocol sets the upstream protocol on the result holder in context, if present.
|
||||||
|
func setUpstreamProtocol(ctx context.Context, protocol string) {
|
||||||
|
if ctx == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r, ok := ctx.Value(upstreamProtocolKey{}).(*upstreamProtocolResult); ok && r != nil {
|
||||||
|
r.protocol = protocol
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type upstreamClient interface {
|
type upstreamClient interface {
|
||||||
exchange(ctx context.Context, upstream string, r *dns.Msg) (*dns.Msg, time.Duration, error)
|
exchange(ctx context.Context, upstream string, r *dns.Msg) (*dns.Msg, time.Duration, error)
|
||||||
}
|
}
|
||||||
@@ -138,7 +189,16 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, failures := u.tryUpstreamServers(w, r, logger)
|
// Propagate inbound protocol so upstream exchange can use TCP directly
|
||||||
|
// when the request came in over TCP.
|
||||||
|
ctx := u.ctx
|
||||||
|
if addr := w.RemoteAddr(); addr != nil {
|
||||||
|
network := addr.Network()
|
||||||
|
ctx = contextWithDNSProtocol(ctx, network)
|
||||||
|
resutil.SetMeta(w, "protocol", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, failures := u.tryUpstreamServers(ctx, w, r, logger)
|
||||||
if len(failures) > 0 {
|
if len(failures) > 0 {
|
||||||
u.logUpstreamFailures(r.Question[0].Name, failures, ok, logger)
|
u.logUpstreamFailures(r.Question[0].Name, failures, ok, logger)
|
||||||
}
|
}
|
||||||
@@ -153,7 +213,7 @@ func (u *upstreamResolverBase) prepareRequest(r *dns.Msg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolverBase) tryUpstreamServers(w dns.ResponseWriter, r *dns.Msg, logger *log.Entry) (bool, []upstreamFailure) {
|
func (u *upstreamResolverBase) tryUpstreamServers(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, logger *log.Entry) (bool, []upstreamFailure) {
|
||||||
timeout := u.upstreamTimeout
|
timeout := u.upstreamTimeout
|
||||||
if len(u.upstreamServers) > 1 {
|
if len(u.upstreamServers) > 1 {
|
||||||
maxTotal := 5 * time.Second
|
maxTotal := 5 * time.Second
|
||||||
@@ -168,7 +228,7 @@ func (u *upstreamResolverBase) tryUpstreamServers(w dns.ResponseWriter, r *dns.M
|
|||||||
|
|
||||||
var failures []upstreamFailure
|
var failures []upstreamFailure
|
||||||
for _, upstream := range u.upstreamServers {
|
for _, upstream := range u.upstreamServers {
|
||||||
if failure := u.queryUpstream(w, r, upstream, timeout, logger); failure != nil {
|
if failure := u.queryUpstream(ctx, w, r, upstream, timeout, logger); failure != nil {
|
||||||
failures = append(failures, *failure)
|
failures = append(failures, *failure)
|
||||||
} else {
|
} else {
|
||||||
return true, failures
|
return true, failures
|
||||||
@@ -178,15 +238,17 @@ func (u *upstreamResolverBase) tryUpstreamServers(w dns.ResponseWriter, r *dns.M
|
|||||||
}
|
}
|
||||||
|
|
||||||
// queryUpstream queries a single upstream server. Returns nil on success, or failure info to try next upstream.
|
// queryUpstream queries a single upstream server. Returns nil on success, or failure info to try next upstream.
|
||||||
func (u *upstreamResolverBase) queryUpstream(w dns.ResponseWriter, r *dns.Msg, upstream netip.AddrPort, timeout time.Duration, logger *log.Entry) *upstreamFailure {
|
func (u *upstreamResolverBase) queryUpstream(parentCtx context.Context, w dns.ResponseWriter, r *dns.Msg, upstream netip.AddrPort, timeout time.Duration, logger *log.Entry) *upstreamFailure {
|
||||||
var rm *dns.Msg
|
var rm *dns.Msg
|
||||||
var t time.Duration
|
var t time.Duration
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
var startTime time.Time
|
var startTime time.Time
|
||||||
|
var upstreamProto *upstreamProtocolResult
|
||||||
func() {
|
func() {
|
||||||
ctx, cancel := context.WithTimeout(u.ctx, timeout)
|
ctx, cancel := context.WithTimeout(parentCtx, timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
ctx, upstreamProto = contextWithupstreamProtocolResult(ctx)
|
||||||
startTime = time.Now()
|
startTime = time.Now()
|
||||||
rm, t, err = u.upstreamClient.exchange(ctx, upstream.String(), r)
|
rm, t, err = u.upstreamClient.exchange(ctx, upstream.String(), r)
|
||||||
}()
|
}()
|
||||||
@@ -203,7 +265,7 @@ func (u *upstreamResolverBase) queryUpstream(w dns.ResponseWriter, r *dns.Msg, u
|
|||||||
return &upstreamFailure{upstream: upstream, reason: dns.RcodeToString[rm.Rcode]}
|
return &upstreamFailure{upstream: upstream, reason: dns.RcodeToString[rm.Rcode]}
|
||||||
}
|
}
|
||||||
|
|
||||||
u.writeSuccessResponse(w, rm, upstream, r.Question[0].Name, t, logger)
|
u.writeSuccessResponse(w, rm, upstream, r.Question[0].Name, t, upstreamProto, logger)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,10 +282,13 @@ func (u *upstreamResolverBase) handleUpstreamError(err error, upstream netip.Add
|
|||||||
return &upstreamFailure{upstream: upstream, reason: reason}
|
return &upstreamFailure{upstream: upstream, reason: reason}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolverBase) writeSuccessResponse(w dns.ResponseWriter, rm *dns.Msg, upstream netip.AddrPort, domain string, t time.Duration, logger *log.Entry) bool {
|
func (u *upstreamResolverBase) writeSuccessResponse(w dns.ResponseWriter, rm *dns.Msg, upstream netip.AddrPort, domain string, t time.Duration, upstreamProto *upstreamProtocolResult, logger *log.Entry) bool {
|
||||||
u.successCount.Add(1)
|
u.successCount.Add(1)
|
||||||
|
|
||||||
resutil.SetMeta(w, "upstream", upstream.String())
|
resutil.SetMeta(w, "upstream", upstream.String())
|
||||||
|
if upstreamProto != nil && upstreamProto.protocol != "" {
|
||||||
|
resutil.SetMeta(w, "upstream_protocol", upstreamProto.protocol)
|
||||||
|
}
|
||||||
|
|
||||||
// Clear Zero bit from external responses to prevent upstream servers from
|
// Clear Zero bit from external responses to prevent upstream servers from
|
||||||
// manipulating our internal fallthrough signaling mechanism
|
// manipulating our internal fallthrough signaling mechanism
|
||||||
@@ -428,13 +493,42 @@ func (u *upstreamResolverBase) testNameserver(baseCtx context.Context, externalC
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clientUDPMaxSize returns the maximum UDP response size the client accepts.
|
||||||
|
func clientUDPMaxSize(r *dns.Msg) int {
|
||||||
|
if opt := r.IsEdns0(); opt != nil {
|
||||||
|
return int(opt.UDPSize())
|
||||||
|
}
|
||||||
|
return dns.MinMsgSize
|
||||||
|
}
|
||||||
|
|
||||||
// ExchangeWithFallback exchanges a DNS message with the upstream server.
|
// ExchangeWithFallback exchanges a DNS message with the upstream server.
|
||||||
// It first tries to use UDP, and if it is truncated, it falls back to TCP.
|
// It first tries to use UDP, and if it is truncated, it falls back to TCP.
|
||||||
|
// If the inbound request came over TCP (via context), it skips the UDP attempt.
|
||||||
// If the passed context is nil, this will use Exchange instead of ExchangeContext.
|
// If the passed context is nil, this will use Exchange instead of ExchangeContext.
|
||||||
func ExchangeWithFallback(ctx context.Context, client *dns.Client, r *dns.Msg, upstream string) (*dns.Msg, time.Duration, error) {
|
func ExchangeWithFallback(ctx context.Context, client *dns.Client, r *dns.Msg, upstream string) (*dns.Msg, time.Duration, error) {
|
||||||
// MTU - ip + udp headers
|
// If the request came in over TCP, go straight to TCP upstream.
|
||||||
// Note: this could be sent out on an interface that is not ours, but higher MTU settings could break truncation handling.
|
if dnsProtocolFromContext(ctx) == protoTCP {
|
||||||
client.UDPSize = uint16(currentMTU - (60 + 8))
|
tcpClient := *client
|
||||||
|
tcpClient.Net = protoTCP
|
||||||
|
rm, t, err := tcpClient.ExchangeContext(ctx, r, upstream)
|
||||||
|
if err != nil {
|
||||||
|
return nil, t, fmt.Errorf("with tcp: %w", err)
|
||||||
|
}
|
||||||
|
setUpstreamProtocol(ctx, protoTCP)
|
||||||
|
return rm, t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
clientMaxSize := clientUDPMaxSize(r)
|
||||||
|
|
||||||
|
// Cap EDNS0 to our tunnel MTU so the upstream doesn't send a
|
||||||
|
// response larger than our read buffer.
|
||||||
|
// Note: the query could be sent out on an interface that is not ours,
|
||||||
|
// but higher MTU settings could break truncation handling.
|
||||||
|
maxUDPPayload := uint16(currentMTU - ipUDPHeaderSize)
|
||||||
|
client.UDPSize = maxUDPPayload
|
||||||
|
if opt := r.IsEdns0(); opt != nil && opt.UDPSize() > maxUDPPayload {
|
||||||
|
opt.SetUDPSize(maxUDPPayload)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
rm *dns.Msg
|
rm *dns.Msg
|
||||||
@@ -453,25 +547,32 @@ func ExchangeWithFallback(ctx context.Context, client *dns.Client, r *dns.Msg, u
|
|||||||
}
|
}
|
||||||
|
|
||||||
if rm == nil || !rm.MsgHdr.Truncated {
|
if rm == nil || !rm.MsgHdr.Truncated {
|
||||||
|
setUpstreamProtocol(ctx, protoUDP)
|
||||||
return rm, t, nil
|
return rm, t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Tracef("udp response for domain=%s type=%v class=%v is truncated, trying TCP.",
|
// TODO: if the upstream's truncated UDP response already contains more
|
||||||
r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass)
|
// data than the client's buffer, we could truncate locally and skip
|
||||||
|
// the TCP retry.
|
||||||
|
|
||||||
client.Net = "tcp"
|
tcpClient := *client
|
||||||
|
tcpClient.Net = protoTCP
|
||||||
|
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
rm, t, err = client.Exchange(r, upstream)
|
rm, t, err = tcpClient.Exchange(r, upstream)
|
||||||
} else {
|
} else {
|
||||||
rm, t, err = client.ExchangeContext(ctx, r, upstream)
|
rm, t, err = tcpClient.ExchangeContext(ctx, r, upstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, t, fmt.Errorf("with tcp: %w", err)
|
return nil, t, fmt.Errorf("with tcp: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: once TCP is implemented, rm.Truncate() if the request came in over UDP
|
setUpstreamProtocol(ctx, protoTCP)
|
||||||
|
|
||||||
|
if rm.Len() > clientMaxSize {
|
||||||
|
rm.Truncate(clientMaxSize)
|
||||||
|
}
|
||||||
|
|
||||||
return rm, t, nil
|
return rm, t, nil
|
||||||
}
|
}
|
||||||
@@ -479,18 +580,46 @@ func ExchangeWithFallback(ctx context.Context, client *dns.Client, r *dns.Msg, u
|
|||||||
// ExchangeWithNetstack performs a DNS exchange using netstack for dialing.
|
// ExchangeWithNetstack performs a DNS exchange using netstack for dialing.
|
||||||
// This is needed when netstack is enabled to reach peer IPs through the tunnel.
|
// This is needed when netstack is enabled to reach peer IPs through the tunnel.
|
||||||
func ExchangeWithNetstack(ctx context.Context, nsNet *netstack.Net, r *dns.Msg, upstream string) (*dns.Msg, error) {
|
func ExchangeWithNetstack(ctx context.Context, nsNet *netstack.Net, r *dns.Msg, upstream string) (*dns.Msg, error) {
|
||||||
reply, err := netstackExchange(ctx, nsNet, r, upstream, "udp")
|
// If request came in over TCP, go straight to TCP upstream
|
||||||
|
if dnsProtocolFromContext(ctx) == protoTCP {
|
||||||
|
rm, err := netstackExchange(ctx, nsNet, r, upstream, protoTCP)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
setUpstreamProtocol(ctx, protoTCP)
|
||||||
|
return rm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
clientMaxSize := clientUDPMaxSize(r)
|
||||||
|
|
||||||
|
// Cap EDNS0 to our tunnel MTU so the upstream doesn't send a
|
||||||
|
// response larger than what we can read over UDP.
|
||||||
|
maxUDPPayload := uint16(currentMTU - ipUDPHeaderSize)
|
||||||
|
if opt := r.IsEdns0(); opt != nil && opt.UDPSize() > maxUDPPayload {
|
||||||
|
opt.SetUDPSize(maxUDPPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := netstackExchange(ctx, nsNet, r, upstream, protoUDP)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If response is truncated, retry with TCP
|
|
||||||
if reply != nil && reply.MsgHdr.Truncated {
|
if reply != nil && reply.MsgHdr.Truncated {
|
||||||
log.Tracef("udp response for domain=%s type=%v class=%v is truncated, trying TCP",
|
rm, err := netstackExchange(ctx, nsNet, r, upstream, protoTCP)
|
||||||
r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass)
|
if err != nil {
|
||||||
return netstackExchange(ctx, nsNet, r, upstream, "tcp")
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
setUpstreamProtocol(ctx, protoTCP)
|
||||||
|
if rm.Len() > clientMaxSize {
|
||||||
|
rm.Truncate(clientMaxSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setUpstreamProtocol(ctx, protoUDP)
|
||||||
|
|
||||||
return reply, nil
|
return reply, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,7 +640,7 @@ func netstackExchange(ctx context.Context, nsNet *netstack.Net, r *dns.Msg, upst
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsConn := &dns.Conn{Conn: conn}
|
dnsConn := &dns.Conn{Conn: conn, UDPSize: uint16(currentMTU - ipUDPHeaderSize)}
|
||||||
|
|
||||||
if err := dnsConn.WriteMsg(r); err != nil {
|
if err := dnsConn.WriteMsg(r); err != nil {
|
||||||
return nil, fmt.Errorf("write %s message: %w", network, err)
|
return nil, fmt.Errorf("write %s message: %w", network, err)
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func (u *upstreamResolver) exchangeWithinVPN(ctx context.Context, upstream strin
|
|||||||
upstreamExchangeClient := &dns.Client{
|
upstreamExchangeClient := &dns.Client{
|
||||||
Timeout: ClientTimeout,
|
Timeout: ClientTimeout,
|
||||||
}
|
}
|
||||||
return upstreamExchangeClient.ExchangeContext(ctx, r, upstream)
|
return ExchangeWithFallback(ctx, upstreamExchangeClient, r, upstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
// exchangeWithoutVPN protect the UDP socket by Android SDK to avoid to goes through the VPN
|
// exchangeWithoutVPN protect the UDP socket by Android SDK to avoid to goes through the VPN
|
||||||
@@ -76,7 +76,7 @@ func (u *upstreamResolver) exchangeWithoutVPN(ctx context.Context, upstream stri
|
|||||||
Timeout: timeout,
|
Timeout: timeout,
|
||||||
}
|
}
|
||||||
|
|
||||||
return upstreamExchangeClient.ExchangeContext(ctx, r, upstream)
|
return ExchangeWithFallback(ctx, upstreamExchangeClient, r, upstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *upstreamResolver) isLocalResolver(upstream string) bool {
|
func (u *upstreamResolver) isLocalResolver(upstream string) bool {
|
||||||
|
|||||||
@@ -475,3 +475,298 @@ func TestFormatFailures(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDNSProtocolContext(t *testing.T) {
|
||||||
|
t.Run("roundtrip udp", func(t *testing.T) {
|
||||||
|
ctx := contextWithDNSProtocol(context.Background(), protoUDP)
|
||||||
|
assert.Equal(t, protoUDP, dnsProtocolFromContext(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("roundtrip tcp", func(t *testing.T) {
|
||||||
|
ctx := contextWithDNSProtocol(context.Background(), protoTCP)
|
||||||
|
assert.Equal(t, protoTCP, dnsProtocolFromContext(ctx))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing returns empty", func(t *testing.T) {
|
||||||
|
assert.Equal(t, "", dnsProtocolFromContext(context.Background()))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExchangeWithFallback_TCPContext(t *testing.T) {
|
||||||
|
// Start a local DNS server that responds on TCP only
|
||||||
|
tcpHandler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetReply(r)
|
||||||
|
m.Answer = append(m.Answer, &dns.A{
|
||||||
|
Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
|
||||||
|
A: net.ParseIP("10.0.0.1"),
|
||||||
|
})
|
||||||
|
if err := w.WriteMsg(m); err != nil {
|
||||||
|
t.Logf("write msg: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tcpServer := &dns.Server{
|
||||||
|
Addr: "127.0.0.1:0",
|
||||||
|
Net: "tcp",
|
||||||
|
Handler: tcpHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
tcpServer.Listener = tcpLn
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := tcpServer.ActivateAndServe(); err != nil {
|
||||||
|
t.Logf("tcp server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer func() {
|
||||||
|
_ = tcpServer.Shutdown()
|
||||||
|
}()
|
||||||
|
|
||||||
|
upstream := tcpLn.Addr().String()
|
||||||
|
|
||||||
|
// With TCP context, should connect directly via TCP without trying UDP
|
||||||
|
ctx := contextWithDNSProtocol(context.Background(), protoTCP)
|
||||||
|
client := &dns.Client{Timeout: 2 * time.Second}
|
||||||
|
r := new(dns.Msg).SetQuestion("example.com.", dns.TypeA)
|
||||||
|
|
||||||
|
rm, _, err := ExchangeWithFallback(ctx, client, r, upstream)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rm)
|
||||||
|
require.NotEmpty(t, rm.Answer)
|
||||||
|
assert.Contains(t, rm.Answer[0].String(), "10.0.0.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExchangeWithFallback_UDPFallbackToTCP(t *testing.T) {
|
||||||
|
// UDP handler returns a truncated response to trigger TCP retry.
|
||||||
|
udpHandler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetReply(r)
|
||||||
|
m.Truncated = true
|
||||||
|
if err := w.WriteMsg(m); err != nil {
|
||||||
|
t.Logf("write msg: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// TCP handler returns the full answer.
|
||||||
|
tcpHandler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetReply(r)
|
||||||
|
m.Answer = append(m.Answer, &dns.A{
|
||||||
|
Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
|
||||||
|
A: net.ParseIP("10.0.0.3"),
|
||||||
|
})
|
||||||
|
if err := w.WriteMsg(m); err != nil {
|
||||||
|
t.Logf("write msg: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
udpPC, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
addr := udpPC.LocalAddr().String()
|
||||||
|
|
||||||
|
udpServer := &dns.Server{
|
||||||
|
PacketConn: udpPC,
|
||||||
|
Net: "udp",
|
||||||
|
Handler: udpHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpLn, err := net.Listen("tcp", addr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tcpServer := &dns.Server{
|
||||||
|
Listener: tcpLn,
|
||||||
|
Net: "tcp",
|
||||||
|
Handler: tcpHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := udpServer.ActivateAndServe(); err != nil {
|
||||||
|
t.Logf("udp server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
if err := tcpServer.ActivateAndServe(); err != nil {
|
||||||
|
t.Logf("tcp server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer func() {
|
||||||
|
_ = udpServer.Shutdown()
|
||||||
|
_ = tcpServer.Shutdown()
|
||||||
|
}()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
client := &dns.Client{Timeout: 2 * time.Second}
|
||||||
|
r := new(dns.Msg).SetQuestion("example.com.", dns.TypeA)
|
||||||
|
|
||||||
|
rm, _, err := ExchangeWithFallback(ctx, client, r, addr)
|
||||||
|
require.NoError(t, err, "should fall back to TCP after truncated UDP response")
|
||||||
|
require.NotNil(t, rm)
|
||||||
|
require.NotEmpty(t, rm.Answer, "TCP response should contain the full answer")
|
||||||
|
assert.Contains(t, rm.Answer[0].String(), "10.0.0.3")
|
||||||
|
assert.False(t, rm.Truncated, "TCP response should not be truncated")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExchangeWithFallback_TCPContextSkipsUDP(t *testing.T) {
|
||||||
|
// Start only a TCP server (no UDP). With TCP context it should succeed.
|
||||||
|
tcpHandler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetReply(r)
|
||||||
|
m.Answer = append(m.Answer, &dns.A{
|
||||||
|
Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
|
||||||
|
A: net.ParseIP("10.0.0.2"),
|
||||||
|
})
|
||||||
|
if err := w.WriteMsg(m); err != nil {
|
||||||
|
t.Logf("write msg: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tcpLn, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tcpServer := &dns.Server{
|
||||||
|
Listener: tcpLn,
|
||||||
|
Net: "tcp",
|
||||||
|
Handler: tcpHandler,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := tcpServer.ActivateAndServe(); err != nil {
|
||||||
|
t.Logf("tcp server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer func() {
|
||||||
|
_ = tcpServer.Shutdown()
|
||||||
|
}()
|
||||||
|
|
||||||
|
upstream := tcpLn.Addr().String()
|
||||||
|
|
||||||
|
// TCP context: should skip UDP entirely and go directly to TCP
|
||||||
|
ctx := contextWithDNSProtocol(context.Background(), protoTCP)
|
||||||
|
client := &dns.Client{Timeout: 2 * time.Second}
|
||||||
|
r := new(dns.Msg).SetQuestion("example.com.", dns.TypeA)
|
||||||
|
|
||||||
|
rm, _, err := ExchangeWithFallback(ctx, client, r, upstream)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rm)
|
||||||
|
require.NotEmpty(t, rm.Answer)
|
||||||
|
assert.Contains(t, rm.Answer[0].String(), "10.0.0.2")
|
||||||
|
|
||||||
|
// Without TCP context, trying to reach a TCP-only server via UDP should fail
|
||||||
|
ctx2 := context.Background()
|
||||||
|
client2 := &dns.Client{Timeout: 500 * time.Millisecond}
|
||||||
|
_, _, err = ExchangeWithFallback(ctx2, client2, r, upstream)
|
||||||
|
assert.Error(t, err, "should fail when no UDP server and no TCP context")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExchangeWithFallback_EDNS0Capped(t *testing.T) {
|
||||||
|
// Verify that a client EDNS0 larger than our MTU-derived limit gets
|
||||||
|
// capped in the outgoing request so the upstream doesn't send a
|
||||||
|
// response larger than our read buffer.
|
||||||
|
var receivedUDPSize uint16
|
||||||
|
udpHandler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
if opt := r.IsEdns0(); opt != nil {
|
||||||
|
receivedUDPSize = opt.UDPSize()
|
||||||
|
}
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetReply(r)
|
||||||
|
m.Answer = append(m.Answer, &dns.A{
|
||||||
|
Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
|
||||||
|
A: net.ParseIP("10.0.0.1"),
|
||||||
|
})
|
||||||
|
if err := w.WriteMsg(m); err != nil {
|
||||||
|
t.Logf("write msg: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
udpPC, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
addr := udpPC.LocalAddr().String()
|
||||||
|
|
||||||
|
udpServer := &dns.Server{PacketConn: udpPC, Net: "udp", Handler: udpHandler}
|
||||||
|
go func() { _ = udpServer.ActivateAndServe() }()
|
||||||
|
t.Cleanup(func() { _ = udpServer.Shutdown() })
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
client := &dns.Client{Timeout: 2 * time.Second}
|
||||||
|
r := new(dns.Msg).SetQuestion("example.com.", dns.TypeA)
|
||||||
|
r.SetEdns0(4096, false)
|
||||||
|
|
||||||
|
rm, _, err := ExchangeWithFallback(ctx, client, r, addr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rm)
|
||||||
|
|
||||||
|
expectedMax := uint16(currentMTU - ipUDPHeaderSize)
|
||||||
|
assert.Equal(t, expectedMax, receivedUDPSize,
|
||||||
|
"upstream should see capped EDNS0, not the client's 4096")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExchangeWithFallback_TCPTruncatesToClientSize(t *testing.T) {
|
||||||
|
// When the client advertises a large EDNS0 (4096) and the upstream
|
||||||
|
// truncates, the TCP response should NOT be truncated since the full
|
||||||
|
// answer fits within the client's original buffer.
|
||||||
|
udpHandler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetReply(r)
|
||||||
|
m.Truncated = true
|
||||||
|
if err := w.WriteMsg(m); err != nil {
|
||||||
|
t.Logf("write msg: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
tcpHandler := dns.HandlerFunc(func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetReply(r)
|
||||||
|
// Add enough records to exceed MTU but fit within 4096
|
||||||
|
for i := range 20 {
|
||||||
|
m.Answer = append(m.Answer, &dns.TXT{
|
||||||
|
Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassINET, Ttl: 60},
|
||||||
|
Txt: []string{fmt.Sprintf("record-%d-padding-data-to-make-it-longer", i)},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := w.WriteMsg(m); err != nil {
|
||||||
|
t.Logf("write msg: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
udpPC, err := net.ListenPacket("udp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
addr := udpPC.LocalAddr().String()
|
||||||
|
|
||||||
|
udpServer := &dns.Server{PacketConn: udpPC, Net: "udp", Handler: udpHandler}
|
||||||
|
tcpLn, err := net.Listen("tcp", addr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
tcpServer := &dns.Server{Listener: tcpLn, Net: "tcp", Handler: tcpHandler}
|
||||||
|
|
||||||
|
go func() { _ = udpServer.ActivateAndServe() }()
|
||||||
|
go func() { _ = tcpServer.ActivateAndServe() }()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = udpServer.Shutdown()
|
||||||
|
_ = tcpServer.Shutdown()
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
client := &dns.Client{Timeout: 2 * time.Second}
|
||||||
|
|
||||||
|
// Client with large buffer: should get all records without truncation
|
||||||
|
r := new(dns.Msg).SetQuestion("example.com.", dns.TypeTXT)
|
||||||
|
r.SetEdns0(4096, false)
|
||||||
|
|
||||||
|
rm, _, err := ExchangeWithFallback(ctx, client, r, addr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rm)
|
||||||
|
assert.Len(t, rm.Answer, 20, "large EDNS0 client should get all records")
|
||||||
|
assert.False(t, rm.Truncated, "response should not be truncated for large buffer client")
|
||||||
|
|
||||||
|
// Client with small buffer: should get truncated response
|
||||||
|
r2 := new(dns.Msg).SetQuestion("example.com.", dns.TypeTXT)
|
||||||
|
r2.SetEdns0(512, false)
|
||||||
|
|
||||||
|
rm2, _, err := ExchangeWithFallback(ctx, &dns.Client{Timeout: 2 * time.Second}, r2, addr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, rm2)
|
||||||
|
assert.Less(t, len(rm2.Answer), 20, "small EDNS0 client should get fewer records")
|
||||||
|
assert.True(t, rm2.Truncated, "response should be truncated for small buffer client")
|
||||||
|
}
|
||||||
|
|||||||
@@ -237,8 +237,8 @@ func (f *DNSForwarder) writeResponse(logger *log.Entry, w dns.ResponseWriter, re
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Tracef("response: domain=%s rcode=%s answers=%s took=%s",
|
logger.Tracef("response: domain=%s rcode=%s answers=%s size=%dB took=%s",
|
||||||
qname, dns.RcodeToString[resp.Rcode], resutil.FormatAnswers(resp.Answer), time.Since(startTime))
|
qname, dns.RcodeToString[resp.Rcode], resutil.FormatAnswers(resp.Answer), resp.Len(), time.Since(startTime))
|
||||||
}
|
}
|
||||||
|
|
||||||
// udpResponseWriter wraps a dns.ResponseWriter to handle UDP-specific truncation.
|
// udpResponseWriter wraps a dns.ResponseWriter to handle UDP-specific truncation.
|
||||||
@@ -263,20 +263,28 @@ func (u *udpResponseWriter) WriteMsg(resp *dns.Msg) error {
|
|||||||
|
|
||||||
func (f *DNSForwarder) handleDNSQueryUDP(w dns.ResponseWriter, query *dns.Msg) {
|
func (f *DNSForwarder) handleDNSQueryUDP(w dns.ResponseWriter, query *dns.Msg) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
logger := log.WithFields(log.Fields{
|
fields := log.Fields{
|
||||||
"request_id": resutil.GenerateRequestID(),
|
"request_id": resutil.GenerateRequestID(),
|
||||||
"dns_id": fmt.Sprintf("%04x", query.Id),
|
"dns_id": fmt.Sprintf("%04x", query.Id),
|
||||||
})
|
}
|
||||||
|
if addr := w.RemoteAddr(); addr != nil {
|
||||||
|
fields["client"] = addr.String()
|
||||||
|
}
|
||||||
|
logger := log.WithFields(fields)
|
||||||
|
|
||||||
f.handleDNSQuery(logger, &udpResponseWriter{ResponseWriter: w, query: query}, query, startTime)
|
f.handleDNSQuery(logger, &udpResponseWriter{ResponseWriter: w, query: query}, query, startTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *DNSForwarder) handleDNSQueryTCP(w dns.ResponseWriter, query *dns.Msg) {
|
func (f *DNSForwarder) handleDNSQueryTCP(w dns.ResponseWriter, query *dns.Msg) {
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
logger := log.WithFields(log.Fields{
|
fields := log.Fields{
|
||||||
"request_id": resutil.GenerateRequestID(),
|
"request_id": resutil.GenerateRequestID(),
|
||||||
"dns_id": fmt.Sprintf("%04x", query.Id),
|
"dns_id": fmt.Sprintf("%04x", query.Id),
|
||||||
})
|
}
|
||||||
|
if addr := w.RemoteAddr(); addr != nil {
|
||||||
|
fields["client"] = addr.String()
|
||||||
|
}
|
||||||
|
logger := log.WithFields(fields)
|
||||||
|
|
||||||
f.handleDNSQuery(logger, w, query, startTime)
|
f.handleDNSQuery(logger, w, query, startTime)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/peer/guard"
|
"github.com/netbirdio/netbird/client/internal/peer/guard"
|
||||||
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
||||||
"github.com/netbirdio/netbird/client/internal/peerstore"
|
"github.com/netbirdio/netbird/client/internal/peerstore"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/portforward"
|
||||||
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
"github.com/netbirdio/netbird/client/internal/profilemanager"
|
||||||
"github.com/netbirdio/netbird/client/internal/relay"
|
"github.com/netbirdio/netbird/client/internal/relay"
|
||||||
"github.com/netbirdio/netbird/client/internal/rosenpass"
|
"github.com/netbirdio/netbird/client/internal/rosenpass"
|
||||||
@@ -210,9 +211,10 @@ type Engine struct {
|
|||||||
// checks are the client-applied posture checks that need to be evaluated on the client
|
// checks are the client-applied posture checks that need to be evaluated on the client
|
||||||
checks []*mgmProto.Checks
|
checks []*mgmProto.Checks
|
||||||
|
|
||||||
relayManager *relayClient.Manager
|
relayManager *relayClient.Manager
|
||||||
stateManager *statemanager.Manager
|
stateManager *statemanager.Manager
|
||||||
srWatcher *guard.SRWatcher
|
portForwardManager *portforward.Manager
|
||||||
|
srWatcher *guard.SRWatcher
|
||||||
|
|
||||||
// Sync response persistence (protected by syncRespMux)
|
// Sync response persistence (protected by syncRespMux)
|
||||||
syncRespMux sync.RWMutex
|
syncRespMux sync.RWMutex
|
||||||
@@ -259,26 +261,27 @@ func NewEngine(
|
|||||||
mobileDep MobileDependency,
|
mobileDep MobileDependency,
|
||||||
) *Engine {
|
) *Engine {
|
||||||
engine := &Engine{
|
engine := &Engine{
|
||||||
clientCtx: clientCtx,
|
clientCtx: clientCtx,
|
||||||
clientCancel: clientCancel,
|
clientCancel: clientCancel,
|
||||||
signal: services.SignalClient,
|
signal: services.SignalClient,
|
||||||
signaler: peer.NewSignaler(services.SignalClient, config.WgPrivateKey),
|
signaler: peer.NewSignaler(services.SignalClient, config.WgPrivateKey),
|
||||||
mgmClient: services.MgmClient,
|
mgmClient: services.MgmClient,
|
||||||
relayManager: services.RelayManager,
|
relayManager: services.RelayManager,
|
||||||
peerStore: peerstore.NewConnStore(),
|
peerStore: peerstore.NewConnStore(),
|
||||||
syncMsgMux: &sync.Mutex{},
|
syncMsgMux: &sync.Mutex{},
|
||||||
config: config,
|
config: config,
|
||||||
mobileDep: mobileDep,
|
mobileDep: mobileDep,
|
||||||
STUNs: []*stun.URI{},
|
STUNs: []*stun.URI{},
|
||||||
TURNs: []*stun.URI{},
|
TURNs: []*stun.URI{},
|
||||||
networkSerial: 0,
|
networkSerial: 0,
|
||||||
statusRecorder: services.StatusRecorder,
|
statusRecorder: services.StatusRecorder,
|
||||||
stateManager: services.StateManager,
|
stateManager: services.StateManager,
|
||||||
checks: services.Checks,
|
portForwardManager: portforward.NewManager(),
|
||||||
probeStunTurn: relay.NewStunTurnProbe(relay.DefaultCacheTTL),
|
checks: services.Checks,
|
||||||
jobExecutor: jobexec.NewExecutor(),
|
probeStunTurn: relay.NewStunTurnProbe(relay.DefaultCacheTTL),
|
||||||
clientMetrics: services.ClientMetrics,
|
jobExecutor: jobexec.NewExecutor(),
|
||||||
updateManager: services.UpdateManager,
|
clientMetrics: services.ClientMetrics,
|
||||||
|
updateManager: services.UpdateManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("I am: %s", config.WgPrivateKey.PublicKey().String())
|
log.Infof("I am: %s", config.WgPrivateKey.PublicKey().String())
|
||||||
@@ -521,6 +524,11 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inject firewall into DNS server now that it's available.
|
||||||
|
// The DNS server is created before the firewall because the route manager
|
||||||
|
// depends on the DNS server, and the firewall depends on the wg interface.
|
||||||
|
e.dnsServer.SetFirewall(e.firewall)
|
||||||
|
|
||||||
e.udpMux, err = e.wgInterface.Up()
|
e.udpMux, err = e.wgInterface.Up()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to pull up wgInterface [%s]: %s", e.wgInterface.Name(), err.Error())
|
log.Errorf("failed to pull up wgInterface [%s]: %s", e.wgInterface.Name(), err.Error())
|
||||||
@@ -532,6 +540,13 @@ func (e *Engine) Start(netbirdConfig *mgmProto.NetbirdConfig, mgmtURL *url.URL)
|
|||||||
// conntrack entries from being created before the rules are in place
|
// conntrack entries from being created before the rules are in place
|
||||||
e.setupWGProxyNoTrack()
|
e.setupWGProxyNoTrack()
|
||||||
|
|
||||||
|
// Start after interface is up since port may have been resolved from 0 or changed if occupied
|
||||||
|
e.shutdownWg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer e.shutdownWg.Done()
|
||||||
|
e.portForwardManager.Start(e.ctx, uint16(e.config.WgPort))
|
||||||
|
}()
|
||||||
|
|
||||||
// Set the WireGuard interface for rosenpass after interface is up
|
// Set the WireGuard interface for rosenpass after interface is up
|
||||||
if e.rpManager != nil {
|
if e.rpManager != nil {
|
||||||
e.rpManager.SetInterface(e.wgInterface)
|
e.rpManager.SetInterface(e.wgInterface)
|
||||||
@@ -1535,12 +1550,13 @@ func (e *Engine) createPeerConn(pubKey string, allowedIPs []netip.Prefix, agentV
|
|||||||
}
|
}
|
||||||
|
|
||||||
serviceDependencies := peer.ServiceDependencies{
|
serviceDependencies := peer.ServiceDependencies{
|
||||||
StatusRecorder: e.statusRecorder,
|
StatusRecorder: e.statusRecorder,
|
||||||
Signaler: e.signaler,
|
Signaler: e.signaler,
|
||||||
IFaceDiscover: e.mobileDep.IFaceDiscover,
|
IFaceDiscover: e.mobileDep.IFaceDiscover,
|
||||||
RelayManager: e.relayManager,
|
RelayManager: e.relayManager,
|
||||||
SrWatcher: e.srWatcher,
|
SrWatcher: e.srWatcher,
|
||||||
MetricsRecorder: e.clientMetrics,
|
PortForwardManager: e.portForwardManager,
|
||||||
|
MetricsRecorder: e.clientMetrics,
|
||||||
}
|
}
|
||||||
peerConn, err := peer.NewConn(config, serviceDependencies)
|
peerConn, err := peer.NewConn(config, serviceDependencies)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1697,6 +1713,12 @@ func (e *Engine) close() {
|
|||||||
if e.rpManager != nil {
|
if e.rpManager != nil {
|
||||||
_ = e.rpManager.Close()
|
_ = e.rpManager.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := e.portForwardManager.GracefullyStop(ctx); err != nil {
|
||||||
|
log.Warnf("failed to gracefully stop port forwarding manager: %s", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, bool, error) {
|
func (e *Engine) readInitialSettings() ([]*route.Route, *nbdns.Config, bool, error) {
|
||||||
@@ -1800,7 +1822,7 @@ func (e *Engine) newDnsServer(dnsConfig *nbdns.Config) (dns.Server, error) {
|
|||||||
return dnsServer, nil
|
return dnsServer, nil
|
||||||
|
|
||||||
case "ios":
|
case "ios":
|
||||||
dnsServer := dns.NewDefaultServerIos(e.ctx, e.wgInterface, e.mobileDep.DnsManager, e.statusRecorder, e.config.DisableDNS)
|
dnsServer := dns.NewDefaultServerIos(e.ctx, e.wgInterface, e.mobileDep.DnsManager, e.mobileDep.HostDNSAddresses, e.statusRecorder, e.config.DisableDNS)
|
||||||
return dnsServer, nil
|
return dnsServer, nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -1837,6 +1859,11 @@ func (e *Engine) GetExposeManager() *expose.Manager {
|
|||||||
return e.exposeManager
|
return e.exposeManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsBlockInbound returns whether inbound connections are blocked.
|
||||||
|
func (e *Engine) IsBlockInbound() bool {
|
||||||
|
return e.config.BlockInbound
|
||||||
|
}
|
||||||
|
|
||||||
// GetClientMetrics returns the client metrics
|
// GetClientMetrics returns the client metrics
|
||||||
func (e *Engine) GetClientMetrics() *metrics.ClientMetrics {
|
func (e *Engine) GetClientMetrics() *metrics.ClientMetrics {
|
||||||
return e.clientMetrics
|
return e.clientMetrics
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer/id"
|
"github.com/netbirdio/netbird/client/internal/peer/id"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer/worker"
|
"github.com/netbirdio/netbird/client/internal/peer/worker"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/portforward"
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
relayClient "github.com/netbirdio/netbird/shared/relay/client"
|
||||||
@@ -45,6 +46,7 @@ type ServiceDependencies struct {
|
|||||||
RelayManager *relayClient.Manager
|
RelayManager *relayClient.Manager
|
||||||
SrWatcher *guard.SRWatcher
|
SrWatcher *guard.SRWatcher
|
||||||
PeerConnDispatcher *dispatcher.ConnectionDispatcher
|
PeerConnDispatcher *dispatcher.ConnectionDispatcher
|
||||||
|
PortForwardManager *portforward.Manager
|
||||||
MetricsRecorder MetricsRecorder
|
MetricsRecorder MetricsRecorder
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,16 +89,17 @@ type ConnConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
Log *log.Entry
|
Log *log.Entry
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
ctxCancel context.CancelFunc
|
ctxCancel context.CancelFunc
|
||||||
config ConnConfig
|
config ConnConfig
|
||||||
statusRecorder *Status
|
statusRecorder *Status
|
||||||
signaler *Signaler
|
signaler *Signaler
|
||||||
iFaceDiscover stdnet.ExternalIFaceDiscover
|
iFaceDiscover stdnet.ExternalIFaceDiscover
|
||||||
relayManager *relayClient.Manager
|
relayManager *relayClient.Manager
|
||||||
srWatcher *guard.SRWatcher
|
srWatcher *guard.SRWatcher
|
||||||
|
portForwardManager *portforward.Manager
|
||||||
|
|
||||||
onConnected func(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string)
|
onConnected func(remoteWireGuardKey string, remoteRosenpassPubKey []byte, wireGuardIP string, remoteRosenpassAddr string)
|
||||||
onDisconnected func(remotePeer string)
|
onDisconnected func(remotePeer string)
|
||||||
@@ -145,19 +148,20 @@ func NewConn(config ConnConfig, services ServiceDependencies) (*Conn, error) {
|
|||||||
|
|
||||||
dumpState := newStateDump(config.Key, connLog, services.StatusRecorder)
|
dumpState := newStateDump(config.Key, connLog, services.StatusRecorder)
|
||||||
var conn = &Conn{
|
var conn = &Conn{
|
||||||
Log: connLog,
|
Log: connLog,
|
||||||
config: config,
|
config: config,
|
||||||
statusRecorder: services.StatusRecorder,
|
statusRecorder: services.StatusRecorder,
|
||||||
signaler: services.Signaler,
|
signaler: services.Signaler,
|
||||||
iFaceDiscover: services.IFaceDiscover,
|
iFaceDiscover: services.IFaceDiscover,
|
||||||
relayManager: services.RelayManager,
|
relayManager: services.RelayManager,
|
||||||
srWatcher: services.SrWatcher,
|
srWatcher: services.SrWatcher,
|
||||||
statusRelay: worker.NewAtomicStatus(),
|
portForwardManager: services.PortForwardManager,
|
||||||
statusICE: worker.NewAtomicStatus(),
|
statusRelay: worker.NewAtomicStatus(),
|
||||||
dumpState: dumpState,
|
statusICE: worker.NewAtomicStatus(),
|
||||||
endpointUpdater: NewEndpointUpdater(connLog, config.WgConfig, isController(config)),
|
dumpState: dumpState,
|
||||||
wgWatcher: NewWGWatcher(connLog, config.WgConfig.WgInterface, config.Key, dumpState),
|
endpointUpdater: NewEndpointUpdater(connLog, config.WgConfig, isController(config)),
|
||||||
metricsRecorder: services.MetricsRecorder,
|
wgWatcher: NewWGWatcher(connLog, config.WgConfig.WgInterface, config.Key, dumpState),
|
||||||
|
metricsRecorder: services.MetricsRecorder,
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn, nil
|
return conn, nil
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/iface/udpmux"
|
"github.com/netbirdio/netbird/client/iface/udpmux"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer/conntype"
|
"github.com/netbirdio/netbird/client/internal/peer/conntype"
|
||||||
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
icemaker "github.com/netbirdio/netbird/client/internal/peer/ice"
|
||||||
|
"github.com/netbirdio/netbird/client/internal/portforward"
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
)
|
)
|
||||||
@@ -61,6 +62,9 @@ type WorkerICE struct {
|
|||||||
|
|
||||||
// we record the last known state of the ICE agent to avoid duplicate on disconnected events
|
// we record the last known state of the ICE agent to avoid duplicate on disconnected events
|
||||||
lastKnownState ice.ConnectionState
|
lastKnownState ice.ConnectionState
|
||||||
|
|
||||||
|
// portForwardAttempted tracks if we've already tried port forwarding this session
|
||||||
|
portForwardAttempted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWorkerICE(ctx context.Context, log *log.Entry, config ConnConfig, conn *Conn, signaler *Signaler, ifaceDiscover stdnet.ExternalIFaceDiscover, statusRecorder *Status, hasRelayOnLocally bool) (*WorkerICE, error) {
|
func NewWorkerICE(ctx context.Context, log *log.Entry, config ConnConfig, conn *Conn, signaler *Signaler, ifaceDiscover stdnet.ExternalIFaceDiscover, statusRecorder *Status, hasRelayOnLocally bool) (*WorkerICE, error) {
|
||||||
@@ -214,6 +218,8 @@ func (w *WorkerICE) Close() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkerICE) reCreateAgent(dialerCancel context.CancelFunc, candidates []ice.CandidateType) (*icemaker.ThreadSafeAgent, error) {
|
func (w *WorkerICE) reCreateAgent(dialerCancel context.CancelFunc, candidates []ice.CandidateType) (*icemaker.ThreadSafeAgent, error) {
|
||||||
|
w.portForwardAttempted = false
|
||||||
|
|
||||||
agent, err := icemaker.NewAgent(w.ctx, w.iFaceDiscover, w.config.ICEConfig, candidates, w.localUfrag, w.localPwd)
|
agent, err := icemaker.NewAgent(w.ctx, w.iFaceDiscover, w.config.ICEConfig, candidates, w.localUfrag, w.localPwd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create agent: %w", err)
|
return nil, fmt.Errorf("create agent: %w", err)
|
||||||
@@ -370,6 +376,93 @@ func (w *WorkerICE) onICECandidate(candidate ice.Candidate) {
|
|||||||
w.log.Errorf("failed signaling candidate to the remote peer %s %s", w.config.Key, err)
|
w.log.Errorf("failed signaling candidate to the remote peer %s %s", w.config.Key, err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if candidate.Type() == ice.CandidateTypeServerReflexive {
|
||||||
|
w.injectPortForwardedCandidate(candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// injectPortForwardedCandidate signals an additional candidate using the pre-created port mapping.
|
||||||
|
func (w *WorkerICE) injectPortForwardedCandidate(srflxCandidate ice.Candidate) {
|
||||||
|
pfManager := w.conn.portForwardManager
|
||||||
|
if pfManager == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping := pfManager.GetMapping()
|
||||||
|
if mapping == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.muxAgent.Lock()
|
||||||
|
if w.portForwardAttempted {
|
||||||
|
w.muxAgent.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.portForwardAttempted = true
|
||||||
|
w.muxAgent.Unlock()
|
||||||
|
|
||||||
|
forwardedCandidate, err := w.createForwardedCandidate(srflxCandidate, mapping)
|
||||||
|
if err != nil {
|
||||||
|
w.log.Warnf("create forwarded candidate: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.log.Debugf("injecting port-forwarded candidate: %s (mapping: %d -> %d via %s, priority: %d)",
|
||||||
|
forwardedCandidate.String(), mapping.InternalPort, mapping.ExternalPort, mapping.NATType, forwardedCandidate.Priority())
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := w.signaler.SignalICECandidate(forwardedCandidate, w.config.Key); err != nil {
|
||||||
|
w.log.Errorf("signal port-forwarded candidate: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// createForwardedCandidate creates a new server reflexive candidate with the forwarded port.
|
||||||
|
// It uses the NAT gateway's external IP with the forwarded port.
|
||||||
|
func (w *WorkerICE) createForwardedCandidate(srflxCandidate ice.Candidate, mapping *portforward.Mapping) (ice.Candidate, error) {
|
||||||
|
var externalIP string
|
||||||
|
if mapping.ExternalIP != nil && !mapping.ExternalIP.IsUnspecified() {
|
||||||
|
externalIP = mapping.ExternalIP.String()
|
||||||
|
} else {
|
||||||
|
// Fallback to STUN-discovered address if NAT didn't provide external IP
|
||||||
|
externalIP = srflxCandidate.Address()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per RFC 8445, the related address for srflx is the base (host candidate address).
|
||||||
|
// If the original srflx has unspecified related address, use its own address as base.
|
||||||
|
relAddr := srflxCandidate.RelatedAddress().Address
|
||||||
|
if relAddr == "" || relAddr == "0.0.0.0" || relAddr == "::" {
|
||||||
|
relAddr = srflxCandidate.Address()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arbitrary +1000 boost on top of RFC 8445 priority to favor port-forwarded candidates
|
||||||
|
// over regular srflx during ICE connectivity checks.
|
||||||
|
priority := srflxCandidate.Priority() + 1000
|
||||||
|
|
||||||
|
candidate, err := ice.NewCandidateServerReflexive(&ice.CandidateServerReflexiveConfig{
|
||||||
|
Network: srflxCandidate.NetworkType().String(),
|
||||||
|
Address: externalIP,
|
||||||
|
Port: int(mapping.ExternalPort),
|
||||||
|
Component: srflxCandidate.Component(),
|
||||||
|
Priority: priority,
|
||||||
|
RelAddr: relAddr,
|
||||||
|
RelPort: int(mapping.InternalPort),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create candidate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range srflxCandidate.Extensions() {
|
||||||
|
if e.Key == ice.ExtensionKeyCandidateID {
|
||||||
|
e.Value = srflxCandidate.ID()
|
||||||
|
}
|
||||||
|
if err := candidate.AddExtension(e); err != nil {
|
||||||
|
return nil, fmt.Errorf("add extension: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkerICE) onICESelectedCandidatePair(agent *icemaker.ThreadSafeAgent, c1, c2 ice.Candidate) {
|
func (w *WorkerICE) onICESelectedCandidatePair(agent *icemaker.ThreadSafeAgent, c1, c2 ice.Candidate) {
|
||||||
@@ -411,10 +504,10 @@ func (w *WorkerICE) logSuccessfulPaths(agent *icemaker.ThreadSafeAgent) {
|
|||||||
if !lok || !rok {
|
if !lok || !rok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
w.log.Debugf("successful ICE path %s: [%s %s %s] <-> [%s %s %s] rtt=%.3fms",
|
w.log.Debugf("successful ICE path %s: [%s %s %s:%d] <-> [%s %s %s:%d] rtt=%.3fms",
|
||||||
sessionID,
|
sessionID,
|
||||||
local.NetworkType(), local.Type(), local.Address(),
|
local.NetworkType(), local.Type(), local.Address(), local.Port(),
|
||||||
remote.NetworkType(), remote.Type(), remote.Address(),
|
remote.NetworkType(), remote.Type(), remote.Address(), remote.Port(),
|
||||||
stat.CurrentRoundTripTime*1000)
|
stat.CurrentRoundTripTime*1000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
client/internal/portforward/env.go
Normal file
35
client/internal/portforward/env.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package portforward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
envDisableNATMapper = "NB_DISABLE_NAT_MAPPER"
|
||||||
|
envDisablePCPHealthCheck = "NB_DISABLE_PCP_HEALTH_CHECK"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isDisabledByEnv() bool {
|
||||||
|
return parseBoolEnv(envDisableNATMapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHealthCheckDisabled() bool {
|
||||||
|
return parseBoolEnv(envDisablePCPHealthCheck)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBoolEnv(key string) bool {
|
||||||
|
val := os.Getenv(key)
|
||||||
|
if val == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
disabled, err := strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to parse %s: %v", key, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return disabled
|
||||||
|
}
|
||||||
342
client/internal/portforward/manager.go
Normal file
342
client/internal/portforward/manager.go
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
//go:build !js
|
||||||
|
|
||||||
|
package portforward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-nat"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/portforward/pcp"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultMappingTTL = 2 * time.Hour
|
||||||
|
healthCheckInterval = 1 * time.Minute
|
||||||
|
discoveryTimeout = 10 * time.Second
|
||||||
|
mappingDescription = "NetBird"
|
||||||
|
)
|
||||||
|
|
||||||
|
// upnpErrPermanentLeaseOnly matches UPnP error 725 in SOAP fault XML,
|
||||||
|
// allowing for whitespace/newlines between tags from different router firmware.
|
||||||
|
var upnpErrPermanentLeaseOnly = regexp.MustCompile(`<errorCode>\s*725\s*</errorCode>`)
|
||||||
|
|
||||||
|
// Mapping represents an active NAT port mapping.
|
||||||
|
type Mapping struct {
|
||||||
|
Protocol string
|
||||||
|
InternalPort uint16
|
||||||
|
ExternalPort uint16
|
||||||
|
ExternalIP net.IP
|
||||||
|
NATType string
|
||||||
|
// TTL is the lease duration. Zero means a permanent lease that never expires.
|
||||||
|
TTL time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: persist mapping state for crash recovery cleanup of permanent leases.
|
||||||
|
// Currently not done because State.Cleanup requires NAT gateway re-discovery,
|
||||||
|
// which blocks startup for ~10s when no gateway is present (affects all clients).
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
mapping *Mapping
|
||||||
|
mappingLock sync.Mutex
|
||||||
|
|
||||||
|
wgPort uint16
|
||||||
|
|
||||||
|
done chan struct{}
|
||||||
|
stopCtx chan context.Context
|
||||||
|
|
||||||
|
// protect exported functions
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager creates a new port forwarding manager.
|
||||||
|
func NewManager() *Manager {
|
||||||
|
return &Manager{
|
||||||
|
stopCtx: make(chan context.Context, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Start(ctx context.Context, wgPort uint16) {
|
||||||
|
m.mu.Lock()
|
||||||
|
if m.cancel != nil {
|
||||||
|
m.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDisabledByEnv() {
|
||||||
|
log.Infof("NAT port mapper disabled via %s", envDisableNATMapper)
|
||||||
|
m.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if wgPort == 0 {
|
||||||
|
log.Warnf("invalid WireGuard port 0; NAT mapping disabled")
|
||||||
|
m.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.wgPort = wgPort
|
||||||
|
|
||||||
|
m.done = make(chan struct{})
|
||||||
|
defer close(m.done)
|
||||||
|
|
||||||
|
ctx, m.cancel = context.WithCancel(ctx)
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
gateway, mapping, err := m.setup(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("port forwarding setup: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mappingLock.Lock()
|
||||||
|
m.mapping = mapping
|
||||||
|
m.mappingLock.Unlock()
|
||||||
|
|
||||||
|
m.renewLoop(ctx, gateway, mapping.TTL)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case cleanupCtx := <-m.stopCtx:
|
||||||
|
// block the Start while cleaned up gracefully
|
||||||
|
m.cleanup(cleanupCtx, gateway)
|
||||||
|
default:
|
||||||
|
// return Start immediately and cleanup in background
|
||||||
|
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
go func() {
|
||||||
|
defer cleanupCancel()
|
||||||
|
m.cleanup(cleanupCtx, gateway)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMapping returns the current mapping if ready, nil otherwise
|
||||||
|
func (m *Manager) GetMapping() *Mapping {
|
||||||
|
m.mappingLock.Lock()
|
||||||
|
defer m.mappingLock.Unlock()
|
||||||
|
|
||||||
|
if m.mapping == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping := *m.mapping
|
||||||
|
return &mapping
|
||||||
|
}
|
||||||
|
|
||||||
|
// GracefullyStop cancels the manager and attempts to delete the port mapping.
|
||||||
|
// After GracefullyStop returns, the manager cannot be restarted.
|
||||||
|
func (m *Manager) GracefullyStop(ctx context.Context) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.cancel == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send cleanup context before cancelling, so Start picks it up after renewLoop exits.
|
||||||
|
m.startTearDown(ctx)
|
||||||
|
|
||||||
|
m.cancel()
|
||||||
|
m.cancel = nil
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-m.done:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) setup(ctx context.Context) (nat.NAT, *Mapping, error) {
|
||||||
|
discoverCtx, discoverCancel := context.WithTimeout(ctx, discoveryTimeout)
|
||||||
|
defer discoverCancel()
|
||||||
|
|
||||||
|
gateway, err := discoverGateway(discoverCtx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("discover gateway: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("discovered NAT gateway: %s", gateway.Type())
|
||||||
|
|
||||||
|
mapping, err := m.createMapping(ctx, gateway)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("create port mapping: %w", err)
|
||||||
|
}
|
||||||
|
return gateway, mapping, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) createMapping(ctx context.Context, gateway nat.NAT) (*Mapping, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ttl := defaultMappingTTL
|
||||||
|
externalPort, err := gateway.AddPortMapping(ctx, "udp", int(m.wgPort), mappingDescription, ttl)
|
||||||
|
if err != nil {
|
||||||
|
if !isPermanentLeaseRequired(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Infof("gateway only supports permanent leases, retrying with indefinite duration")
|
||||||
|
ttl = 0
|
||||||
|
externalPort, err = gateway.AddPortMapping(ctx, "udp", int(m.wgPort), mappingDescription, ttl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
externalIP, err := gateway.GetExternalAddress()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("failed to get external address: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping := &Mapping{
|
||||||
|
Protocol: "udp",
|
||||||
|
InternalPort: m.wgPort,
|
||||||
|
ExternalPort: uint16(externalPort),
|
||||||
|
ExternalIP: externalIP,
|
||||||
|
NATType: gateway.Type(),
|
||||||
|
TTL: ttl,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("created port mapping: %d -> %d via %s (external IP: %s)",
|
||||||
|
m.wgPort, externalPort, gateway.Type(), externalIP)
|
||||||
|
return mapping, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) renewLoop(ctx context.Context, gateway nat.NAT, ttl time.Duration) {
|
||||||
|
if ttl == 0 {
|
||||||
|
// Permanent mappings don't expire, just wait for cancellation
|
||||||
|
// but still run health checks for PCP gateways.
|
||||||
|
m.permanentLeaseLoop(ctx, gateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renewTicker := time.NewTicker(ttl / 2)
|
||||||
|
healthTicker := time.NewTicker(healthCheckInterval)
|
||||||
|
defer renewTicker.Stop()
|
||||||
|
defer healthTicker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-renewTicker.C:
|
||||||
|
if err := m.renewMapping(ctx, gateway); err != nil {
|
||||||
|
log.Warnf("failed to renew port mapping: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
case <-healthTicker.C:
|
||||||
|
if m.checkHealthAndRecreate(ctx, gateway) {
|
||||||
|
renewTicker.Reset(ttl / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) permanentLeaseLoop(ctx context.Context, gateway nat.NAT) {
|
||||||
|
healthTicker := time.NewTicker(healthCheckInterval)
|
||||||
|
defer healthTicker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-healthTicker.C:
|
||||||
|
m.checkHealthAndRecreate(ctx, gateway)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) checkHealthAndRecreate(ctx context.Context, gateway nat.NAT) bool {
|
||||||
|
if isHealthCheckDisabled() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mappingLock.Lock()
|
||||||
|
hasMapping := m.mapping != nil
|
||||||
|
m.mappingLock.Unlock()
|
||||||
|
|
||||||
|
if !hasMapping {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
pcpNAT, ok := gateway.(*pcp.NAT)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
epoch, serverRestarted, err := pcpNAT.CheckServerHealth(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("PCP health check failed: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if serverRestarted {
|
||||||
|
log.Warnf("PCP server restart detected (epoch=%d), recreating port mapping", epoch)
|
||||||
|
if err := m.renewMapping(ctx, gateway); err != nil {
|
||||||
|
log.Errorf("failed to recreate port mapping after server restart: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) renewMapping(ctx context.Context, gateway nat.NAT) error {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
externalPort, err := gateway.AddPortMapping(ctx, m.mapping.Protocol, int(m.mapping.InternalPort), mappingDescription, m.mapping.TTL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("add port mapping: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if uint16(externalPort) != m.mapping.ExternalPort {
|
||||||
|
log.Warnf("external port changed on renewal: %d -> %d (candidate may be stale)", m.mapping.ExternalPort, externalPort)
|
||||||
|
m.mappingLock.Lock()
|
||||||
|
m.mapping.ExternalPort = uint16(externalPort)
|
||||||
|
m.mappingLock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("renewed port mapping: %d -> %d", m.mapping.InternalPort, m.mapping.ExternalPort)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) cleanup(ctx context.Context, gateway nat.NAT) {
|
||||||
|
m.mappingLock.Lock()
|
||||||
|
mapping := m.mapping
|
||||||
|
m.mapping = nil
|
||||||
|
m.mappingLock.Unlock()
|
||||||
|
|
||||||
|
if mapping == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gateway.DeletePortMapping(ctx, mapping.Protocol, int(mapping.InternalPort)); err != nil {
|
||||||
|
log.Warnf("delete port mapping on stop: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("deleted port mapping for port %d", mapping.InternalPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) startTearDown(ctx context.Context) {
|
||||||
|
select {
|
||||||
|
case m.stopCtx <- ctx:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPermanentLeaseRequired checks if a UPnP error indicates the gateway only supports permanent leases (error 725).
|
||||||
|
func isPermanentLeaseRequired(err error) bool {
|
||||||
|
return err != nil && upnpErrPermanentLeaseOnly.MatchString(err.Error())
|
||||||
|
}
|
||||||
39
client/internal/portforward/manager_js.go
Normal file
39
client/internal/portforward/manager_js.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package portforward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mapping represents an active NAT port mapping.
|
||||||
|
type Mapping struct {
|
||||||
|
Protocol string
|
||||||
|
InternalPort uint16
|
||||||
|
ExternalPort uint16
|
||||||
|
ExternalIP net.IP
|
||||||
|
NATType string
|
||||||
|
// TTL is the lease duration. Zero means a permanent lease that never expires.
|
||||||
|
TTL time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manager is a stub for js/wasm builds where NAT-PMP/UPnP is not supported.
|
||||||
|
type Manager struct{}
|
||||||
|
|
||||||
|
// NewManager returns a stub manager for js/wasm builds.
|
||||||
|
func NewManager() *Manager {
|
||||||
|
return &Manager{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start is a no-op on js/wasm: NAT-PMP/UPnP is not available in browser environments.
|
||||||
|
func (m *Manager) Start(context.Context, uint16) {
|
||||||
|
// no NAT traversal in wasm
|
||||||
|
}
|
||||||
|
|
||||||
|
// GracefullyStop is a no-op on js/wasm.
|
||||||
|
func (m *Manager) GracefullyStop(context.Context) error { return nil }
|
||||||
|
|
||||||
|
// GetMapping always returns nil on js/wasm.
|
||||||
|
func (m *Manager) GetMapping() *Mapping {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
201
client/internal/portforward/manager_test.go
Normal file
201
client/internal/portforward/manager_test.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
//go:build !js
|
||||||
|
|
||||||
|
package portforward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockNAT struct {
|
||||||
|
natType string
|
||||||
|
deviceAddr net.IP
|
||||||
|
externalAddr net.IP
|
||||||
|
internalAddr net.IP
|
||||||
|
mappings map[int]int
|
||||||
|
addMappingErr error
|
||||||
|
deleteMappingErr error
|
||||||
|
onlyPermanentLeases bool
|
||||||
|
lastTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockNAT() *mockNAT {
|
||||||
|
return &mockNAT{
|
||||||
|
natType: "Mock-NAT",
|
||||||
|
deviceAddr: net.ParseIP("192.168.1.1"),
|
||||||
|
externalAddr: net.ParseIP("203.0.113.50"),
|
||||||
|
internalAddr: net.ParseIP("192.168.1.100"),
|
||||||
|
mappings: make(map[int]int),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNAT) Type() string {
|
||||||
|
return m.natType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNAT) GetDeviceAddress() (net.IP, error) {
|
||||||
|
return m.deviceAddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNAT) GetExternalAddress() (net.IP, error) {
|
||||||
|
return m.externalAddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNAT) GetInternalAddress() (net.IP, error) {
|
||||||
|
return m.internalAddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNAT) AddPortMapping(ctx context.Context, protocol string, internalPort int, description string, timeout time.Duration) (int, error) {
|
||||||
|
if m.addMappingErr != nil {
|
||||||
|
return 0, m.addMappingErr
|
||||||
|
}
|
||||||
|
if m.onlyPermanentLeases && timeout != 0 {
|
||||||
|
return 0, fmt.Errorf("SOAP fault. Code: | Explanation: | Detail: <UPnPError xmlns=\"urn:schemas-upnp-org:control-1-0\"><errorCode>725</errorCode><errorDescription>OnlyPermanentLeasesSupported</errorDescription></UPnPError>")
|
||||||
|
}
|
||||||
|
externalPort := internalPort
|
||||||
|
m.mappings[internalPort] = externalPort
|
||||||
|
m.lastTimeout = timeout
|
||||||
|
return externalPort, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockNAT) DeletePortMapping(ctx context.Context, protocol string, internalPort int) error {
|
||||||
|
if m.deleteMappingErr != nil {
|
||||||
|
return m.deleteMappingErr
|
||||||
|
}
|
||||||
|
delete(m.mappings, internalPort)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManager_CreateMapping(t *testing.T) {
|
||||||
|
m := NewManager()
|
||||||
|
m.wgPort = 51820
|
||||||
|
|
||||||
|
gateway := newMockNAT()
|
||||||
|
mapping, err := m.createMapping(context.Background(), gateway)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, mapping)
|
||||||
|
|
||||||
|
assert.Equal(t, "udp", mapping.Protocol)
|
||||||
|
assert.Equal(t, uint16(51820), mapping.InternalPort)
|
||||||
|
assert.Equal(t, uint16(51820), mapping.ExternalPort)
|
||||||
|
assert.Equal(t, "Mock-NAT", mapping.NATType)
|
||||||
|
assert.Equal(t, net.ParseIP("203.0.113.50").To4(), mapping.ExternalIP.To4())
|
||||||
|
assert.Equal(t, defaultMappingTTL, mapping.TTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManager_GetMapping_ReturnsNilWhenNotReady(t *testing.T) {
|
||||||
|
m := NewManager()
|
||||||
|
assert.Nil(t, m.GetMapping())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManager_GetMapping_ReturnsCopy(t *testing.T) {
|
||||||
|
m := NewManager()
|
||||||
|
m.mapping = &Mapping{
|
||||||
|
Protocol: "udp",
|
||||||
|
InternalPort: 51820,
|
||||||
|
ExternalPort: 51820,
|
||||||
|
}
|
||||||
|
|
||||||
|
mapping := m.GetMapping()
|
||||||
|
require.NotNil(t, mapping)
|
||||||
|
assert.Equal(t, uint16(51820), mapping.InternalPort)
|
||||||
|
|
||||||
|
// Mutating the returned copy should not affect the manager's mapping.
|
||||||
|
mapping.ExternalPort = 9999
|
||||||
|
assert.Equal(t, uint16(51820), m.GetMapping().ExternalPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManager_Cleanup_DeletesMapping(t *testing.T) {
|
||||||
|
m := NewManager()
|
||||||
|
m.mapping = &Mapping{
|
||||||
|
Protocol: "udp",
|
||||||
|
InternalPort: 51820,
|
||||||
|
ExternalPort: 51820,
|
||||||
|
}
|
||||||
|
|
||||||
|
gateway := newMockNAT()
|
||||||
|
// Seed the mock so we can verify deletion.
|
||||||
|
gateway.mappings[51820] = 51820
|
||||||
|
|
||||||
|
m.cleanup(context.Background(), gateway)
|
||||||
|
|
||||||
|
_, exists := gateway.mappings[51820]
|
||||||
|
assert.False(t, exists, "mapping should be deleted from gateway")
|
||||||
|
assert.Nil(t, m.GetMapping(), "in-memory mapping should be cleared")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManager_Cleanup_NilMapping(t *testing.T) {
|
||||||
|
m := NewManager()
|
||||||
|
gateway := newMockNAT()
|
||||||
|
|
||||||
|
// Should not panic or call gateway.
|
||||||
|
m.cleanup(context.Background(), gateway)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func TestManager_CreateMapping_PermanentLeaseFallback(t *testing.T) {
|
||||||
|
m := NewManager()
|
||||||
|
m.wgPort = 51820
|
||||||
|
|
||||||
|
gateway := newMockNAT()
|
||||||
|
gateway.onlyPermanentLeases = true
|
||||||
|
|
||||||
|
mapping, err := m.createMapping(context.Background(), gateway)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, mapping)
|
||||||
|
|
||||||
|
assert.Equal(t, uint16(51820), mapping.InternalPort)
|
||||||
|
assert.Equal(t, time.Duration(0), mapping.TTL, "should return zero TTL for permanent lease")
|
||||||
|
assert.Equal(t, time.Duration(0), gateway.lastTimeout, "should have retried with zero duration")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsPermanentLeaseRequired(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil error",
|
||||||
|
err: nil,
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UPnP error 725",
|
||||||
|
err: fmt.Errorf("SOAP fault. Code: | Detail: <UPnPError><errorCode>725</errorCode><errorDescription>OnlyPermanentLeasesSupported</errorDescription></UPnPError>"),
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrapped error with 725",
|
||||||
|
err: fmt.Errorf("add port mapping: %w", fmt.Errorf("Detail: <errorCode>725</errorCode>")),
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error 725 with newlines in XML",
|
||||||
|
err: fmt.Errorf("<errorCode>\n 725\n</errorCode>"),
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "bare 725 without XML tag",
|
||||||
|
err: fmt.Errorf("error code 725"),
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unrelated error",
|
||||||
|
err: fmt.Errorf("connection refused"),
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.expected, isPermanentLeaseRequired(tt.err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
408
client/internal/portforward/pcp/client.go
Normal file
408
client/internal/portforward/pcp/client.go
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
package pcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultTimeout = 3 * time.Second
|
||||||
|
responseBufferSize = 128
|
||||||
|
|
||||||
|
// RFC 6887 Section 8.1.1 retry timing
|
||||||
|
initialRetryDelay = 3 * time.Second
|
||||||
|
maxRetryDelay = 1024 * time.Second
|
||||||
|
maxRetries = 4 // 3s + 6s + 12s + 24s = 45s total worst case
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is a PCP protocol client.
|
||||||
|
// All methods are safe for concurrent use.
|
||||||
|
type Client struct {
|
||||||
|
gateway netip.Addr
|
||||||
|
timeout time.Duration
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
// localIP caches the resolved local IP address.
|
||||||
|
localIP netip.Addr
|
||||||
|
// lastEpoch is the last observed server epoch value.
|
||||||
|
lastEpoch uint32
|
||||||
|
// epochTime tracks when lastEpoch was received for state loss detection.
|
||||||
|
epochTime time.Time
|
||||||
|
// externalIP caches the external IP from the last successful MAP response.
|
||||||
|
externalIP netip.Addr
|
||||||
|
// epochStateLost is set when epoch indicates server restart.
|
||||||
|
epochStateLost bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient creates a new PCP client for the gateway at the given IP.
|
||||||
|
func NewClient(gateway net.IP) *Client {
|
||||||
|
addr, ok := netip.AddrFromSlice(gateway)
|
||||||
|
if !ok {
|
||||||
|
log.Debugf("invalid gateway IP: %v", gateway)
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
gateway: addr.Unmap(),
|
||||||
|
timeout: defaultTimeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClientWithTimeout creates a new PCP client with a custom timeout.
|
||||||
|
func NewClientWithTimeout(gateway net.IP, timeout time.Duration) *Client {
|
||||||
|
addr, ok := netip.AddrFromSlice(gateway)
|
||||||
|
if !ok {
|
||||||
|
log.Debugf("invalid gateway IP: %v", gateway)
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
gateway: addr.Unmap(),
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLocalIP sets the local IP address to use in PCP requests.
|
||||||
|
func (c *Client) SetLocalIP(ip net.IP) {
|
||||||
|
addr, ok := netip.AddrFromSlice(ip)
|
||||||
|
if !ok {
|
||||||
|
log.Debugf("invalid local IP: %v", ip)
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
c.localIP = addr.Unmap()
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gateway returns the gateway IP address.
|
||||||
|
func (c *Client) Gateway() net.IP {
|
||||||
|
return c.gateway.AsSlice()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Announce sends a PCP ANNOUNCE request to discover PCP support.
|
||||||
|
// Returns the server's epoch time on success.
|
||||||
|
func (c *Client) Announce(ctx context.Context) (epoch uint32, err error) {
|
||||||
|
localIP, err := c.getLocalIP()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("get local IP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req := buildAnnounceRequest(localIP)
|
||||||
|
resp, err := c.sendRequest(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("send announce: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := parseResponse(resp)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("parse announce response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsed.ResultCode != ResultSuccess {
|
||||||
|
return 0, fmt.Errorf("PCP ANNOUNCE failed: %s", ResultCodeString(parsed.ResultCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.updateEpochLocked(parsed.Epoch) {
|
||||||
|
log.Warnf("PCP server epoch indicates state loss - mappings may need refresh")
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
return parsed.Epoch, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPortMapping requests a port mapping from the PCP server.
|
||||||
|
func (c *Client) AddPortMapping(ctx context.Context, protocol string, internalPort int, lifetime time.Duration) (*MapResponse, error) {
|
||||||
|
return c.addPortMappingWithHint(ctx, protocol, internalPort, internalPort, netip.Addr{}, lifetime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPortMappingWithHint requests a port mapping with suggested external port and IP.
|
||||||
|
// Use lifetime <= 0 to delete a mapping.
|
||||||
|
func (c *Client) AddPortMappingWithHint(ctx context.Context, protocol string, internalPort, suggestedExtPort int, suggestedExtIP net.IP, lifetime time.Duration) (*MapResponse, error) {
|
||||||
|
var extIP netip.Addr
|
||||||
|
if suggestedExtIP != nil {
|
||||||
|
var ok bool
|
||||||
|
extIP, ok = netip.AddrFromSlice(suggestedExtIP)
|
||||||
|
if !ok {
|
||||||
|
log.Debugf("invalid suggested external IP: %v", suggestedExtIP)
|
||||||
|
}
|
||||||
|
extIP = extIP.Unmap()
|
||||||
|
}
|
||||||
|
return c.addPortMappingWithHint(ctx, protocol, internalPort, suggestedExtPort, extIP, lifetime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) addPortMappingWithHint(ctx context.Context, protocol string, internalPort, suggestedExtPort int, suggestedExtIP netip.Addr, lifetime time.Duration) (*MapResponse, error) {
|
||||||
|
localIP, err := c.getLocalIP()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get local IP: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proto, err := protocolNumber(protocol)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse protocol: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonce [12]byte
|
||||||
|
if _, err := rand.Read(nonce[:]); err != nil {
|
||||||
|
return nil, fmt.Errorf("generate nonce: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert lifetime to seconds. Lifetime 0 means delete, so only apply
|
||||||
|
// default for positive durations that round to 0 seconds.
|
||||||
|
var lifetimeSec uint32
|
||||||
|
if lifetime > 0 {
|
||||||
|
lifetimeSec = uint32(lifetime.Seconds())
|
||||||
|
if lifetimeSec == 0 {
|
||||||
|
lifetimeSec = DefaultLifetime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req := buildMapRequest(localIP, nonce, proto, uint16(internalPort), uint16(suggestedExtPort), suggestedExtIP, lifetimeSec)
|
||||||
|
|
||||||
|
resp, err := c.sendRequest(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("send map request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapResp, err := parseMapResponse(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse map response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mapResp.Nonce != nonce {
|
||||||
|
return nil, fmt.Errorf("nonce mismatch in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mapResp.Protocol != proto {
|
||||||
|
return nil, fmt.Errorf("protocol mismatch: requested %d, got %d", proto, mapResp.Protocol)
|
||||||
|
}
|
||||||
|
if mapResp.InternalPort != uint16(internalPort) {
|
||||||
|
return nil, fmt.Errorf("internal port mismatch: requested %d, got %d", internalPort, mapResp.InternalPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
if mapResp.ResultCode != ResultSuccess {
|
||||||
|
return nil, &Error{
|
||||||
|
Code: mapResp.ResultCode,
|
||||||
|
Message: ResultCodeString(mapResp.ResultCode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.updateEpochLocked(mapResp.Epoch) {
|
||||||
|
log.Warnf("PCP server epoch indicates state loss - mappings may need refresh")
|
||||||
|
}
|
||||||
|
c.cacheExternalIPLocked(mapResp.ExternalIP)
|
||||||
|
c.mu.Unlock()
|
||||||
|
return mapResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePortMapping removes a port mapping by requesting zero lifetime.
|
||||||
|
func (c *Client) DeletePortMapping(ctx context.Context, protocol string, internalPort int) error {
|
||||||
|
if _, err := c.addPortMappingWithHint(ctx, protocol, internalPort, 0, netip.Addr{}, 0); err != nil {
|
||||||
|
var pcpErr *Error
|
||||||
|
if errors.As(err, &pcpErr) && pcpErr.Code == ResultNotAuthorized {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("delete mapping: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExternalAddress returns the external IP address.
|
||||||
|
// First checks for a cached value from previous MAP responses.
|
||||||
|
// If not cached, creates a short-lived mapping to discover the external IP.
|
||||||
|
func (c *Client) GetExternalAddress(ctx context.Context) (net.IP, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.externalIP.IsValid() {
|
||||||
|
ip := c.externalIP.AsSlice()
|
||||||
|
c.mu.Unlock()
|
||||||
|
return ip, nil
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
// Use an ephemeral port in the dynamic range (49152-65535).
|
||||||
|
// Port 0 is not valid with UDP/TCP protocols per RFC 6887.
|
||||||
|
ephemeralPort := 49152 + int(uint16(time.Now().UnixNano()))%(65535-49152)
|
||||||
|
|
||||||
|
// Use minimal lifetime (1 second) for discovery.
|
||||||
|
resp, err := c.AddPortMapping(ctx, "udp", ephemeralPort, time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create temporary mapping: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.DeletePortMapping(ctx, "udp", ephemeralPort); err != nil {
|
||||||
|
log.Debugf("cleanup temporary PCP mapping: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.ExternalIP.AsSlice(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LastEpoch returns the last observed server epoch value.
|
||||||
|
// A decrease in epoch indicates the server may have restarted and mappings may be lost.
|
||||||
|
func (c *Client) LastEpoch() uint32 {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return c.lastEpoch
|
||||||
|
}
|
||||||
|
|
||||||
|
// EpochStateLost returns true if epoch state loss was detected and clears the flag.
|
||||||
|
func (c *Client) EpochStateLost() bool {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
lost := c.epochStateLost
|
||||||
|
c.epochStateLost = false
|
||||||
|
return lost
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateEpoch updates the epoch tracking and detects potential state loss.
|
||||||
|
// Returns true if state loss was detected (server likely restarted).
|
||||||
|
// Caller must hold c.mu.
|
||||||
|
func (c *Client) updateEpochLocked(newEpoch uint32) bool {
|
||||||
|
now := time.Now()
|
||||||
|
stateLost := false
|
||||||
|
|
||||||
|
// RFC 6887 Section 8.5: Detect invalid epoch indicating server state loss.
|
||||||
|
// client_delta = time since last response
|
||||||
|
// server_delta = epoch change since last response
|
||||||
|
// Invalid if: client_delta+2 < server_delta - server_delta/16
|
||||||
|
// OR: server_delta+2 < client_delta - client_delta/16
|
||||||
|
// The +2 handles quantization, /16 (6.25%) handles clock drift.
|
||||||
|
if !c.epochTime.IsZero() && c.lastEpoch > 0 {
|
||||||
|
clientDelta := uint32(now.Sub(c.epochTime).Seconds())
|
||||||
|
serverDelta := newEpoch - c.lastEpoch
|
||||||
|
|
||||||
|
// Check for epoch going backwards or jumping unexpectedly.
|
||||||
|
// Subtraction is safe: serverDelta/16 is always <= serverDelta.
|
||||||
|
if clientDelta+2 < serverDelta-(serverDelta/16) ||
|
||||||
|
serverDelta+2 < clientDelta-(clientDelta/16) {
|
||||||
|
stateLost = true
|
||||||
|
c.epochStateLost = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.lastEpoch = newEpoch
|
||||||
|
c.epochTime = now
|
||||||
|
return stateLost
|
||||||
|
}
|
||||||
|
|
||||||
|
// cacheExternalIP stores the external IP from a successful MAP response.
|
||||||
|
// Caller must hold c.mu.
|
||||||
|
func (c *Client) cacheExternalIPLocked(ip netip.Addr) {
|
||||||
|
if ip.IsValid() && !ip.IsUnspecified() {
|
||||||
|
c.externalIP = ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendRequest sends a PCP request with retries per RFC 6887 Section 8.1.1.
|
||||||
|
func (c *Client) sendRequest(ctx context.Context, req []byte) ([]byte, error) {
|
||||||
|
addr := &net.UDPAddr{IP: c.gateway.AsSlice(), Port: Port}
|
||||||
|
|
||||||
|
var lastErr error
|
||||||
|
delay := initialRetryDelay
|
||||||
|
|
||||||
|
for range maxRetries {
|
||||||
|
resp, err := c.sendOnce(ctx, addr, req)
|
||||||
|
if err == nil {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
lastErr = err
|
||||||
|
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC 6887 Section 8.1.1: RT = (1 + RAND) * MIN(2 * RTprev, MRT)
|
||||||
|
// RAND is random between -0.1 and +0.1
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
case <-time.After(retryDelayWithJitter(delay)):
|
||||||
|
}
|
||||||
|
delay = min(delay*2, maxRetryDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("PCP request failed after %d retries: %w", maxRetries, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// retryDelayWithJitter applies RFC 6887 jitter: multiply by (1 + RAND) where RAND is [-0.1, +0.1].
|
||||||
|
func retryDelayWithJitter(d time.Duration) time.Duration {
|
||||||
|
var b [1]byte
|
||||||
|
_, _ = rand.Read(b[:])
|
||||||
|
// Convert byte to range [-0.1, +0.1]: (b/255 * 0.2) - 0.1
|
||||||
|
jitter := (float64(b[0])/255.0)*0.2 - 0.1
|
||||||
|
return time.Duration(float64(d) * (1 + jitter))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) sendOnce(ctx context.Context, addr *net.UDPAddr, req []byte) ([]byte, error) {
|
||||||
|
// Use ListenUDP instead of DialUDP to validate response source address per RFC 6887 §8.3.
|
||||||
|
conn, err := net.ListenUDP("udp", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("listen: %w", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
log.Debugf("close UDP connection: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
timeout := c.timeout
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
if remaining := time.Until(deadline); remaining < timeout {
|
||||||
|
timeout = remaining
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil {
|
||||||
|
return nil, fmt.Errorf("set deadline: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := conn.WriteToUDP(req, addr); err != nil {
|
||||||
|
return nil, fmt.Errorf("write: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := make([]byte, responseBufferSize)
|
||||||
|
n, from, err := conn.ReadFromUDP(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC 6887 §8.3: Validate response came from expected PCP server.
|
||||||
|
if !from.IP.Equal(addr.IP) {
|
||||||
|
return nil, fmt.Errorf("response from unexpected source %s (expected %s)", from.IP, addr.IP)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) getLocalIP() (netip.Addr, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if !c.localIP.IsValid() {
|
||||||
|
return netip.Addr{}, fmt.Errorf("local IP not set for gateway %s", c.gateway)
|
||||||
|
}
|
||||||
|
return c.localIP, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func protocolNumber(protocol string) (uint8, error) {
|
||||||
|
switch protocol {
|
||||||
|
case "udp", "UDP":
|
||||||
|
return ProtoUDP, nil
|
||||||
|
case "tcp", "TCP":
|
||||||
|
return ProtoTCP, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unsupported protocol: %s", protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error represents a PCP error response.
|
||||||
|
type Error struct {
|
||||||
|
Code uint8
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return fmt.Sprintf("PCP error: %s (%d)", e.Message, e.Code)
|
||||||
|
}
|
||||||
187
client/internal/portforward/pcp/client_test.go
Normal file
187
client/internal/portforward/pcp/client_test.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package pcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAddrConversion(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
addr netip.Addr
|
||||||
|
}{
|
||||||
|
{"IPv4", netip.MustParseAddr("192.168.1.100")},
|
||||||
|
{"IPv4 loopback", netip.MustParseAddr("127.0.0.1")},
|
||||||
|
{"IPv6", netip.MustParseAddr("2001:db8::1")},
|
||||||
|
{"IPv6 loopback", netip.MustParseAddr("::1")},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
b16 := addrTo16(tt.addr)
|
||||||
|
|
||||||
|
recovered := addrFrom16(b16)
|
||||||
|
assert.Equal(t, tt.addr, recovered, "address should round-trip")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildAnnounceRequest(t *testing.T) {
|
||||||
|
clientIP := netip.MustParseAddr("192.168.1.100")
|
||||||
|
req := buildAnnounceRequest(clientIP)
|
||||||
|
|
||||||
|
require.Len(t, req, headerSize)
|
||||||
|
assert.Equal(t, byte(Version), req[0], "version")
|
||||||
|
assert.Equal(t, byte(OpAnnounce), req[1], "opcode")
|
||||||
|
|
||||||
|
// Check client IP is properly encoded as IPv4-mapped IPv6
|
||||||
|
assert.Equal(t, byte(0xff), req[18], "IPv4-mapped prefix byte 10")
|
||||||
|
assert.Equal(t, byte(0xff), req[19], "IPv4-mapped prefix byte 11")
|
||||||
|
assert.Equal(t, byte(192), req[20], "IP octet 1")
|
||||||
|
assert.Equal(t, byte(168), req[21], "IP octet 2")
|
||||||
|
assert.Equal(t, byte(1), req[22], "IP octet 3")
|
||||||
|
assert.Equal(t, byte(100), req[23], "IP octet 4")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildMapRequest(t *testing.T) {
|
||||||
|
clientIP := netip.MustParseAddr("192.168.1.100")
|
||||||
|
nonce := [12]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
|
||||||
|
req := buildMapRequest(clientIP, nonce, ProtoUDP, 51820, 51820, netip.Addr{}, 3600)
|
||||||
|
|
||||||
|
require.Len(t, req, mapRequestSize)
|
||||||
|
assert.Equal(t, byte(Version), req[0], "version")
|
||||||
|
assert.Equal(t, byte(OpMap), req[1], "opcode")
|
||||||
|
|
||||||
|
// Lifetime at bytes 4-7
|
||||||
|
assert.Equal(t, uint32(3600), (uint32(req[4])<<24)|(uint32(req[5])<<16)|(uint32(req[6])<<8)|uint32(req[7]), "lifetime")
|
||||||
|
|
||||||
|
// Nonce at bytes 24-35
|
||||||
|
assert.Equal(t, nonce[:], req[24:36], "nonce")
|
||||||
|
|
||||||
|
// Protocol at byte 36
|
||||||
|
assert.Equal(t, byte(ProtoUDP), req[36], "protocol")
|
||||||
|
|
||||||
|
// Internal port at bytes 40-41
|
||||||
|
assert.Equal(t, uint16(51820), (uint16(req[40])<<8)|uint16(req[41]), "internal port")
|
||||||
|
|
||||||
|
// External port at bytes 42-43
|
||||||
|
assert.Equal(t, uint16(51820), (uint16(req[42])<<8)|uint16(req[43]), "external port")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseResponse(t *testing.T) {
|
||||||
|
// Construct a valid ANNOUNCE response
|
||||||
|
resp := make([]byte, headerSize)
|
||||||
|
resp[0] = Version
|
||||||
|
resp[1] = OpAnnounce | OpReply
|
||||||
|
// Result code = 0 (success)
|
||||||
|
// Lifetime = 0
|
||||||
|
// Epoch = 12345
|
||||||
|
resp[8] = 0
|
||||||
|
resp[9] = 0
|
||||||
|
resp[10] = 0x30
|
||||||
|
resp[11] = 0x39
|
||||||
|
|
||||||
|
parsed, err := parseResponse(resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, uint8(Version), parsed.Version)
|
||||||
|
assert.Equal(t, uint8(OpAnnounce|OpReply), parsed.Opcode)
|
||||||
|
assert.Equal(t, uint8(ResultSuccess), parsed.ResultCode)
|
||||||
|
assert.Equal(t, uint32(12345), parsed.Epoch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseResponseErrors(t *testing.T) {
|
||||||
|
t.Run("too short", func(t *testing.T) {
|
||||||
|
_, err := parseResponse([]byte{1, 2, 3})
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wrong version", func(t *testing.T) {
|
||||||
|
resp := make([]byte, headerSize)
|
||||||
|
resp[0] = 1 // Wrong version
|
||||||
|
resp[1] = OpReply
|
||||||
|
_, err := parseResponse(resp)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing reply bit", func(t *testing.T) {
|
||||||
|
resp := make([]byte, headerSize)
|
||||||
|
resp[0] = Version
|
||||||
|
resp[1] = OpAnnounce // Missing OpReply bit
|
||||||
|
_, err := parseResponse(resp)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResultCodeString(t *testing.T) {
|
||||||
|
assert.Equal(t, "SUCCESS", ResultCodeString(ResultSuccess))
|
||||||
|
assert.Equal(t, "NOT_AUTHORIZED", ResultCodeString(ResultNotAuthorized))
|
||||||
|
assert.Equal(t, "ADDRESS_MISMATCH", ResultCodeString(ResultAddressMismatch))
|
||||||
|
assert.Contains(t, ResultCodeString(255), "UNKNOWN")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProtocolNumber(t *testing.T) {
|
||||||
|
proto, err := protocolNumber("udp")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, uint8(ProtoUDP), proto)
|
||||||
|
|
||||||
|
proto, err = protocolNumber("tcp")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, uint8(ProtoTCP), proto)
|
||||||
|
|
||||||
|
proto, err = protocolNumber("UDP")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, uint8(ProtoUDP), proto)
|
||||||
|
|
||||||
|
_, err = protocolNumber("icmp")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientCreation(t *testing.T) {
|
||||||
|
gateway := netip.MustParseAddr("192.168.1.1").AsSlice()
|
||||||
|
|
||||||
|
client := NewClient(gateway)
|
||||||
|
assert.Equal(t, net.IP(gateway), client.Gateway())
|
||||||
|
assert.Equal(t, defaultTimeout, client.timeout)
|
||||||
|
|
||||||
|
clientWithTimeout := NewClientWithTimeout(gateway, 5*time.Second)
|
||||||
|
assert.Equal(t, 5*time.Second, clientWithTimeout.timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNATType(t *testing.T) {
|
||||||
|
n := NewNAT(netip.MustParseAddr("192.168.1.1").AsSlice(), netip.MustParseAddr("192.168.1.100").AsSlice())
|
||||||
|
assert.Equal(t, "PCP", n.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Integration test - skipped unless PCP_TEST_GATEWAY env is set
|
||||||
|
func TestClientIntegration(t *testing.T) {
|
||||||
|
t.Skip("Integration test - run manually with PCP_TEST_GATEWAY=<gateway-ip>")
|
||||||
|
|
||||||
|
gateway := netip.MustParseAddr("10.0.1.1").AsSlice() // Change to your test gateway
|
||||||
|
localIP := netip.MustParseAddr("10.0.1.100").AsSlice() // Change to your local IP
|
||||||
|
|
||||||
|
client := NewClient(gateway)
|
||||||
|
client.SetLocalIP(localIP)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Test ANNOUNCE
|
||||||
|
epoch, err := client.Announce(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Logf("Server epoch: %d", epoch)
|
||||||
|
|
||||||
|
// Test MAP
|
||||||
|
resp, err := client.AddPortMapping(ctx, "udp", 51820, 1*time.Hour)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Logf("Mapping: internal=%d external=%d externalIP=%s",
|
||||||
|
resp.InternalPort, resp.ExternalPort, resp.ExternalIP)
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
err = client.DeletePortMapping(ctx, "udp", 51820)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
209
client/internal/portforward/pcp/nat.go
Normal file
209
client/internal/portforward/pcp/nat.go
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
package pcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-nat"
|
||||||
|
"github.com/libp2p/go-netroute"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ nat.NAT = (*NAT)(nil)
|
||||||
|
|
||||||
|
// NAT implements the go-nat NAT interface using PCP.
|
||||||
|
// Supports dual-stack (IPv4 and IPv6) when available.
|
||||||
|
// All methods are safe for concurrent use.
|
||||||
|
//
|
||||||
|
// TODO: IPv6 pinholes use the local IPv6 address. If the address changes
|
||||||
|
// (e.g., due to SLAAC rotation or network change), the pinhole becomes stale
|
||||||
|
// and needs to be recreated with the new address.
|
||||||
|
type NAT struct {
|
||||||
|
client *Client
|
||||||
|
|
||||||
|
mu sync.RWMutex
|
||||||
|
// client6 is the IPv6 PCP client, nil if IPv6 is unavailable.
|
||||||
|
client6 *Client
|
||||||
|
// localIP6 caches the local IPv6 address used for PCP requests.
|
||||||
|
localIP6 netip.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNAT creates a new NAT instance backed by PCP.
|
||||||
|
func NewNAT(gateway, localIP net.IP) *NAT {
|
||||||
|
client := NewClient(gateway)
|
||||||
|
client.SetLocalIP(localIP)
|
||||||
|
return &NAT{
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns "PCP" as the NAT type.
|
||||||
|
func (n *NAT) Type() string {
|
||||||
|
return "PCP"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDeviceAddress returns the gateway IP address.
|
||||||
|
func (n *NAT) GetDeviceAddress() (net.IP, error) {
|
||||||
|
return n.client.Gateway(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExternalAddress returns the external IP address.
|
||||||
|
func (n *NAT) GetExternalAddress() (net.IP, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
return n.client.GetExternalAddress(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInternalAddress returns the local IP address used to communicate with the gateway.
|
||||||
|
func (n *NAT) GetInternalAddress() (net.IP, error) {
|
||||||
|
addr, err := n.client.getLocalIP()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return addr.AsSlice(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPortMapping creates a port mapping on both IPv4 and IPv6 (if available).
|
||||||
|
func (n *NAT) AddPortMapping(ctx context.Context, protocol string, internalPort int, _ string, timeout time.Duration) (int, error) {
|
||||||
|
resp, err := n.client.AddPortMapping(ctx, protocol, internalPort, timeout)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("add mapping: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n.mu.RLock()
|
||||||
|
client6 := n.client6
|
||||||
|
localIP6 := n.localIP6
|
||||||
|
n.mu.RUnlock()
|
||||||
|
|
||||||
|
if client6 == nil {
|
||||||
|
return int(resp.ExternalPort), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := client6.AddPortMapping(ctx, protocol, internalPort, timeout); err != nil {
|
||||||
|
log.Warnf("IPv6 PCP mapping failed (continuing with IPv4): %v", err)
|
||||||
|
return int(resp.ExternalPort), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("created IPv6 PCP pinhole: %s:%d", localIP6, internalPort)
|
||||||
|
return int(resp.ExternalPort), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePortMapping removes a port mapping from both IPv4 and IPv6.
|
||||||
|
func (n *NAT) DeletePortMapping(ctx context.Context, protocol string, internalPort int) error {
|
||||||
|
err := n.client.DeletePortMapping(ctx, protocol, internalPort)
|
||||||
|
|
||||||
|
n.mu.RLock()
|
||||||
|
client6 := n.client6
|
||||||
|
n.mu.RUnlock()
|
||||||
|
|
||||||
|
if client6 != nil {
|
||||||
|
if err6 := client6.DeletePortMapping(ctx, protocol, internalPort); err6 != nil {
|
||||||
|
log.Warnf("IPv6 PCP delete mapping failed: %v", err6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete mapping: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckServerHealth sends an ANNOUNCE to verify the server is still responsive.
|
||||||
|
// Returns the current epoch and whether the server may have restarted (epoch state loss detected).
|
||||||
|
func (n *NAT) CheckServerHealth(ctx context.Context) (epoch uint32, serverRestarted bool, err error) {
|
||||||
|
epoch, err = n.client.Announce(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false, fmt.Errorf("announce: %w", err)
|
||||||
|
}
|
||||||
|
return epoch, n.client.EpochStateLost(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiscoverPCP attempts to discover a PCP-capable gateway.
|
||||||
|
// Returns a NAT interface if PCP is supported, or an error otherwise.
|
||||||
|
// Discovers both IPv4 and IPv6 gateways when available.
|
||||||
|
func DiscoverPCP(ctx context.Context) (nat.NAT, error) {
|
||||||
|
gateway, localIP, err := getDefaultGateway()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get default gateway: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := NewClient(gateway)
|
||||||
|
client.SetLocalIP(localIP)
|
||||||
|
if _, err := client.Announce(ctx); err != nil {
|
||||||
|
return nil, fmt.Errorf("PCP announce: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := &NAT{client: client}
|
||||||
|
discoverIPv6(ctx, result)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func discoverIPv6(ctx context.Context, result *NAT) {
|
||||||
|
gateway6, localIP6, err := getDefaultGateway6()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("IPv6 gateway discovery failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client6 := NewClient(gateway6)
|
||||||
|
client6.SetLocalIP(localIP6)
|
||||||
|
if _, err := client6.Announce(ctx); err != nil {
|
||||||
|
log.Debugf("PCP IPv6 announce failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, ok := netip.AddrFromSlice(localIP6)
|
||||||
|
if !ok {
|
||||||
|
log.Debugf("invalid IPv6 local IP: %v", localIP6)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result.mu.Lock()
|
||||||
|
result.client6 = client6
|
||||||
|
result.localIP6 = addr
|
||||||
|
result.mu.Unlock()
|
||||||
|
log.Debugf("PCP IPv6 gateway discovered: %s (local: %s)", gateway6, localIP6)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultGateway returns the default IPv4 gateway and local IP using the system routing table.
|
||||||
|
func getDefaultGateway() (gateway net.IP, localIP net.IP, err error) {
|
||||||
|
router, err := netroute.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, gateway, localIP, err = router.Route(net.IPv4zero)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if gateway == nil {
|
||||||
|
return nil, nil, nat.ErrNoNATFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return gateway, localIP, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultGateway6 returns the default IPv6 gateway IP address using the system routing table.
|
||||||
|
func getDefaultGateway6() (gateway net.IP, localIP net.IP, err error) {
|
||||||
|
router, err := netroute.New()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, gateway, localIP, err = router.Route(net.IPv6zero)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if gateway == nil {
|
||||||
|
return nil, nil, nat.ErrNoNATFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return gateway, localIP, nil
|
||||||
|
}
|
||||||
225
client/internal/portforward/pcp/protocol.go
Normal file
225
client/internal/portforward/pcp/protocol.go
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
// Package pcp implements the Port Control Protocol (RFC 6887).
|
||||||
|
//
|
||||||
|
// # Implemented Features
|
||||||
|
//
|
||||||
|
// - ANNOUNCE opcode: Discovers PCP server support
|
||||||
|
// - MAP opcode: Creates/deletes port mappings (IPv4 NAT) and firewall pinholes (IPv6)
|
||||||
|
// - Dual-stack: Simultaneous IPv4 and IPv6 support via separate clients
|
||||||
|
// - Nonce validation: Prevents response spoofing
|
||||||
|
// - Epoch tracking: Detects server restarts per Section 8.5
|
||||||
|
// - RFC-compliant retry timing: 3s initial, exponential backoff to 1024s max (Section 8.1.1)
|
||||||
|
//
|
||||||
|
// # Not Implemented
|
||||||
|
//
|
||||||
|
// - PEER opcode: For outbound peer connections (not needed for inbound NAT traversal)
|
||||||
|
// - THIRD_PARTY option: For managing mappings on behalf of other devices
|
||||||
|
// - PREFER_FAILURE option: Requires exact external port or fail (IPv4 NAT only, not needed for IPv6 pinholing)
|
||||||
|
// - FILTER option: To restrict remote peer addresses
|
||||||
|
//
|
||||||
|
// These optional features are omitted because the primary use case is simple
|
||||||
|
// port forwarding for WireGuard, which only requires MAP with default behavior.
|
||||||
|
package pcp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Version is the PCP protocol version (RFC 6887).
|
||||||
|
Version = 2
|
||||||
|
|
||||||
|
// Port is the standard PCP server port.
|
||||||
|
Port = 5351
|
||||||
|
|
||||||
|
// DefaultLifetime is the default requested mapping lifetime in seconds.
|
||||||
|
DefaultLifetime = 7200 // 2 hours
|
||||||
|
|
||||||
|
// Header sizes
|
||||||
|
headerSize = 24
|
||||||
|
mapPayloadSize = 36
|
||||||
|
mapRequestSize = headerSize + mapPayloadSize // 60 bytes
|
||||||
|
)
|
||||||
|
|
||||||
|
// Opcodes
|
||||||
|
const (
|
||||||
|
OpAnnounce = 0
|
||||||
|
OpMap = 1
|
||||||
|
OpPeer = 2
|
||||||
|
OpReply = 0x80 // OR'd with opcode in responses
|
||||||
|
)
|
||||||
|
|
||||||
|
// Protocol numbers for MAP requests
|
||||||
|
const (
|
||||||
|
ProtoUDP = 17
|
||||||
|
ProtoTCP = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
// Result codes (RFC 6887 Section 7.4)
|
||||||
|
const (
|
||||||
|
ResultSuccess = 0
|
||||||
|
ResultUnsuppVersion = 1
|
||||||
|
ResultNotAuthorized = 2
|
||||||
|
ResultMalformedRequest = 3
|
||||||
|
ResultUnsuppOpcode = 4
|
||||||
|
ResultUnsuppOption = 5
|
||||||
|
ResultMalformedOption = 6
|
||||||
|
ResultNetworkFailure = 7
|
||||||
|
ResultNoResources = 8
|
||||||
|
ResultUnsuppProtocol = 9
|
||||||
|
ResultUserExQuota = 10
|
||||||
|
ResultCannotProvideExt = 11
|
||||||
|
ResultAddressMismatch = 12
|
||||||
|
ResultExcessiveRemotePeers = 13
|
||||||
|
)
|
||||||
|
|
||||||
|
// ResultCodeString returns a human-readable string for a result code.
|
||||||
|
func ResultCodeString(code uint8) string {
|
||||||
|
switch code {
|
||||||
|
case ResultSuccess:
|
||||||
|
return "SUCCESS"
|
||||||
|
case ResultUnsuppVersion:
|
||||||
|
return "UNSUPP_VERSION"
|
||||||
|
case ResultNotAuthorized:
|
||||||
|
return "NOT_AUTHORIZED"
|
||||||
|
case ResultMalformedRequest:
|
||||||
|
return "MALFORMED_REQUEST"
|
||||||
|
case ResultUnsuppOpcode:
|
||||||
|
return "UNSUPP_OPCODE"
|
||||||
|
case ResultUnsuppOption:
|
||||||
|
return "UNSUPP_OPTION"
|
||||||
|
case ResultMalformedOption:
|
||||||
|
return "MALFORMED_OPTION"
|
||||||
|
case ResultNetworkFailure:
|
||||||
|
return "NETWORK_FAILURE"
|
||||||
|
case ResultNoResources:
|
||||||
|
return "NO_RESOURCES"
|
||||||
|
case ResultUnsuppProtocol:
|
||||||
|
return "UNSUPP_PROTOCOL"
|
||||||
|
case ResultUserExQuota:
|
||||||
|
return "USER_EX_QUOTA"
|
||||||
|
case ResultCannotProvideExt:
|
||||||
|
return "CANNOT_PROVIDE_EXTERNAL"
|
||||||
|
case ResultAddressMismatch:
|
||||||
|
return "ADDRESS_MISMATCH"
|
||||||
|
case ResultExcessiveRemotePeers:
|
||||||
|
return "EXCESSIVE_REMOTE_PEERS"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("UNKNOWN(%d)", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response represents a parsed PCP response header.
|
||||||
|
type Response struct {
|
||||||
|
Version uint8
|
||||||
|
Opcode uint8
|
||||||
|
ResultCode uint8
|
||||||
|
Lifetime uint32
|
||||||
|
Epoch uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapResponse contains the full response to a MAP request.
|
||||||
|
type MapResponse struct {
|
||||||
|
Response
|
||||||
|
Nonce [12]byte
|
||||||
|
Protocol uint8
|
||||||
|
InternalPort uint16
|
||||||
|
ExternalPort uint16
|
||||||
|
ExternalIP netip.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// addrTo16 converts an address to its 16-byte IPv4-mapped IPv6 representation.
|
||||||
|
func addrTo16(addr netip.Addr) [16]byte {
|
||||||
|
if addr.Is4() {
|
||||||
|
return netip.AddrFrom4(addr.As4()).As16()
|
||||||
|
}
|
||||||
|
return addr.As16()
|
||||||
|
}
|
||||||
|
|
||||||
|
// addrFrom16 extracts an address from a 16-byte representation, unmapping IPv4.
|
||||||
|
func addrFrom16(b [16]byte) netip.Addr {
|
||||||
|
return netip.AddrFrom16(b).Unmap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildAnnounceRequest creates a PCP ANNOUNCE request packet.
|
||||||
|
func buildAnnounceRequest(clientIP netip.Addr) []byte {
|
||||||
|
req := make([]byte, headerSize)
|
||||||
|
req[0] = Version
|
||||||
|
req[1] = OpAnnounce
|
||||||
|
mapped := addrTo16(clientIP)
|
||||||
|
copy(req[8:24], mapped[:])
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildMapRequest creates a PCP MAP request packet.
|
||||||
|
func buildMapRequest(clientIP netip.Addr, nonce [12]byte, protocol uint8, internalPort, suggestedExtPort uint16, suggestedExtIP netip.Addr, lifetime uint32) []byte {
|
||||||
|
req := make([]byte, mapRequestSize)
|
||||||
|
|
||||||
|
// Header
|
||||||
|
req[0] = Version
|
||||||
|
req[1] = OpMap
|
||||||
|
binary.BigEndian.PutUint32(req[4:8], lifetime)
|
||||||
|
mapped := addrTo16(clientIP)
|
||||||
|
copy(req[8:24], mapped[:])
|
||||||
|
|
||||||
|
// MAP payload
|
||||||
|
copy(req[24:36], nonce[:])
|
||||||
|
req[36] = protocol
|
||||||
|
binary.BigEndian.PutUint16(req[40:42], internalPort)
|
||||||
|
binary.BigEndian.PutUint16(req[42:44], suggestedExtPort)
|
||||||
|
if suggestedExtIP.IsValid() {
|
||||||
|
extMapped := addrTo16(suggestedExtIP)
|
||||||
|
copy(req[44:60], extMapped[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseResponse parses the common PCP response header.
|
||||||
|
func parseResponse(data []byte) (*Response, error) {
|
||||||
|
if len(data) < headerSize {
|
||||||
|
return nil, fmt.Errorf("response too short: %d bytes", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &Response{
|
||||||
|
Version: data[0],
|
||||||
|
Opcode: data[1],
|
||||||
|
ResultCode: data[3], // Byte 2 is reserved, byte 3 is result code (RFC 6887 §7.2)
|
||||||
|
Lifetime: binary.BigEndian.Uint32(data[4:8]),
|
||||||
|
Epoch: binary.BigEndian.Uint32(data[8:12]),
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Version != Version {
|
||||||
|
return nil, fmt.Errorf("unsupported PCP version: %d", resp.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Opcode&OpReply == 0 {
|
||||||
|
return nil, fmt.Errorf("response missing reply bit: opcode=0x%02x", resp.Opcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseMapResponse parses a complete MAP response.
|
||||||
|
func parseMapResponse(data []byte) (*MapResponse, error) {
|
||||||
|
if len(data) < mapRequestSize {
|
||||||
|
return nil, fmt.Errorf("MAP response too short: %d bytes", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := parseResponse(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse header: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mapResp := &MapResponse{
|
||||||
|
Response: *resp,
|
||||||
|
Protocol: data[36],
|
||||||
|
InternalPort: binary.BigEndian.Uint16(data[40:42]),
|
||||||
|
ExternalPort: binary.BigEndian.Uint16(data[42:44]),
|
||||||
|
ExternalIP: addrFrom16([16]byte(data[44:60])),
|
||||||
|
}
|
||||||
|
copy(mapResp.Nonce[:], data[24:36])
|
||||||
|
|
||||||
|
return mapResp, nil
|
||||||
|
}
|
||||||
63
client/internal/portforward/state.go
Normal file
63
client/internal/portforward/state.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
//go:build !js
|
||||||
|
|
||||||
|
package portforward
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/libp2p/go-nat"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/internal/portforward/pcp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// discoverGateway is the function used for NAT gateway discovery.
|
||||||
|
// It can be replaced in tests to avoid real network operations.
|
||||||
|
// Tries PCP first, then falls back to NAT-PMP/UPnP.
|
||||||
|
var discoverGateway = defaultDiscoverGateway
|
||||||
|
|
||||||
|
func defaultDiscoverGateway(ctx context.Context) (nat.NAT, error) {
|
||||||
|
pcpGateway, err := pcp.DiscoverPCP(ctx)
|
||||||
|
if err == nil {
|
||||||
|
return pcpGateway, nil
|
||||||
|
}
|
||||||
|
log.Debugf("PCP discovery failed: %v, trying NAT-PMP/UPnP", err)
|
||||||
|
|
||||||
|
return nat.DiscoverGateway(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// State is persisted only for crash recovery cleanup
|
||||||
|
type State struct {
|
||||||
|
InternalPort uint16 `json:"internal_port,omitempty"`
|
||||||
|
Protocol string `json:"protocol,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *State) Name() string {
|
||||||
|
return "port_forward_state"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup implements statemanager.CleanableState for crash recovery
|
||||||
|
func (s *State) Cleanup() error {
|
||||||
|
if s.InternalPort == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("cleaning up stale port mapping for port %d", s.InternalPort)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), discoveryTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
gateway, err := discoverGateway(ctx)
|
||||||
|
if err != nil {
|
||||||
|
// Discovery failure is not an error - gateway may not exist
|
||||||
|
log.Debugf("cleanup: no gateway found: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gateway.DeletePortMapping(ctx, s.Protocol, int(s.InternalPort)); err != nil {
|
||||||
|
return fmt.Errorf("delete port mapping: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -168,6 +168,7 @@ func (m *DefaultManager) setupAndroidRoutes(config ManagerConfig) {
|
|||||||
NetworkType: route.IPv4Network,
|
NetworkType: route.IPv4Network,
|
||||||
}
|
}
|
||||||
cr = append(cr, fakeIPRoute)
|
cr = append(cr, fakeIPRoute)
|
||||||
|
m.notifier.SetFakeIPRoute(fakeIPRoute)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.notifier.SetInitialClientRoutes(cr, routesForComparison)
|
m.notifier.SetInitialClientRoutes(cr, routesForComparison)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
type Notifier struct {
|
type Notifier struct {
|
||||||
initialRoutes []*route.Route
|
initialRoutes []*route.Route
|
||||||
currentRoutes []*route.Route
|
currentRoutes []*route.Route
|
||||||
|
fakeIPRoute *route.Route
|
||||||
|
|
||||||
listener listener.NetworkChangeListener
|
listener listener.NetworkChangeListener
|
||||||
listenerMux sync.Mutex
|
listenerMux sync.Mutex
|
||||||
@@ -31,13 +32,17 @@ func (n *Notifier) SetListener(listener listener.NetworkChangeListener) {
|
|||||||
n.listener = listener
|
n.listener = listener
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetInitialClientRoutes stores the full initial route set (including fake IP blocks)
|
// SetInitialClientRoutes stores the initial route sets for TUN configuration.
|
||||||
// and a separate comparison set (without fake IP blocks) for diff detection.
|
|
||||||
func (n *Notifier) SetInitialClientRoutes(initialRoutes []*route.Route, routesForComparison []*route.Route) {
|
func (n *Notifier) SetInitialClientRoutes(initialRoutes []*route.Route, routesForComparison []*route.Route) {
|
||||||
n.initialRoutes = filterStatic(initialRoutes)
|
n.initialRoutes = filterStatic(initialRoutes)
|
||||||
n.currentRoutes = filterStatic(routesForComparison)
|
n.currentRoutes = filterStatic(routesForComparison)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetFakeIPRoute stores the fake IP route to be included in every TUN rebuild.
|
||||||
|
func (n *Notifier) SetFakeIPRoute(r *route.Route) {
|
||||||
|
n.fakeIPRoute = r
|
||||||
|
}
|
||||||
|
|
||||||
func (n *Notifier) OnNewRoutes(idMap route.HAMap) {
|
func (n *Notifier) OnNewRoutes(idMap route.HAMap) {
|
||||||
var newRoutes []*route.Route
|
var newRoutes []*route.Route
|
||||||
for _, routes := range idMap {
|
for _, routes := range idMap {
|
||||||
@@ -69,7 +74,9 @@ func (n *Notifier) notify() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
allRoutes := slices.Clone(n.currentRoutes)
|
allRoutes := slices.Clone(n.currentRoutes)
|
||||||
allRoutes = append(allRoutes, n.extraInitialRoutes()...)
|
if n.fakeIPRoute != nil {
|
||||||
|
allRoutes = append(allRoutes, n.fakeIPRoute)
|
||||||
|
}
|
||||||
|
|
||||||
routeStrings := n.routesToStrings(allRoutes)
|
routeStrings := n.routesToStrings(allRoutes)
|
||||||
sort.Strings(routeStrings)
|
sort.Strings(routeStrings)
|
||||||
@@ -78,23 +85,6 @@ func (n *Notifier) notify() {
|
|||||||
}(n.listener)
|
}(n.listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
// extraInitialRoutes returns initialRoutes whose network prefix is absent
|
|
||||||
// from currentRoutes (e.g. the fake IP block added at setup time).
|
|
||||||
func (n *Notifier) extraInitialRoutes() []*route.Route {
|
|
||||||
currentNets := make(map[netip.Prefix]struct{}, len(n.currentRoutes))
|
|
||||||
for _, r := range n.currentRoutes {
|
|
||||||
currentNets[r.Network] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var extra []*route.Route
|
|
||||||
for _, r := range n.initialRoutes {
|
|
||||||
if _, ok := currentNets[r.Network]; !ok {
|
|
||||||
extra = append(extra, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return extra
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterStatic(routes []*route.Route) []*route.Route {
|
func filterStatic(routes []*route.Route) []*route.Route {
|
||||||
out := make([]*route.Route, 0, len(routes))
|
out := make([]*route.Route, 0, len(routes))
|
||||||
for _, r := range routes {
|
for _, r := range routes {
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ func (n *Notifier) SetInitialClientRoutes([]*route.Route, []*route.Route) {
|
|||||||
// iOS doesn't care about initial routes
|
// iOS doesn't care about initial routes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *Notifier) SetFakeIPRoute(*route.Route) {
|
||||||
|
// Not used on iOS
|
||||||
|
}
|
||||||
|
|
||||||
func (n *Notifier) OnNewRoutes(route.HAMap) {
|
func (n *Notifier) OnNewRoutes(route.HAMap) {
|
||||||
// Not used on iOS
|
// Not used on iOS
|
||||||
}
|
}
|
||||||
@@ -53,7 +57,6 @@ func (n *Notifier) OnNewPrefixes(prefixes []netip.Prefix) {
|
|||||||
n.currentPrefixes = newNets
|
n.currentPrefixes = newNets
|
||||||
n.notify()
|
n.notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notifier) notify() {
|
func (n *Notifier) notify() {
|
||||||
n.listenerMux.Lock()
|
n.listenerMux.Lock()
|
||||||
defer n.listenerMux.Unlock()
|
defer n.listenerMux.Unlock()
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ func (n *Notifier) SetInitialClientRoutes([]*route.Route, []*route.Route) {
|
|||||||
// Not used on non-mobile platforms
|
// Not used on non-mobile platforms
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *Notifier) SetFakeIPRoute(*route.Route) {
|
||||||
|
// Not used on non-mobile platforms
|
||||||
|
}
|
||||||
|
|
||||||
func (n *Notifier) OnNewRoutes(idMap route.HAMap) {
|
func (n *Notifier) OnNewRoutes(idMap route.HAMap) {
|
||||||
// Not used on non-mobile platforms
|
// Not used on non-mobile platforms
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,7 +161,11 @@ func (c *Client) Run(fd int32, interfaceName string, envList *EnvList) error {
|
|||||||
cfg.WgIface = interfaceName
|
cfg.WgIface = interfaceName
|
||||||
|
|
||||||
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
|
c.connectClient = internal.NewConnectClient(ctx, cfg, c.recorder)
|
||||||
return c.connectClient.RunOniOS(fd, c.networkChangeListener, c.dnsManager, c.stateFile)
|
hostDNS := []netip.AddrPort{
|
||||||
|
netip.MustParseAddrPort("9.9.9.9:53"),
|
||||||
|
netip.MustParseAddrPort("149.112.112.112:53"),
|
||||||
|
}
|
||||||
|
return c.connectClient.RunOniOS(fd, c.networkChangeListener, c.dnsManager, hostDNS, c.stateFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop the internal client and free the resources
|
// Stop the internal client and free the resources
|
||||||
|
|||||||
@@ -4979,6 +4979,7 @@ type GetFeaturesResponse struct {
|
|||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
DisableProfiles bool `protobuf:"varint,1,opt,name=disable_profiles,json=disableProfiles,proto3" json:"disable_profiles,omitempty"`
|
DisableProfiles bool `protobuf:"varint,1,opt,name=disable_profiles,json=disableProfiles,proto3" json:"disable_profiles,omitempty"`
|
||||||
DisableUpdateSettings bool `protobuf:"varint,2,opt,name=disable_update_settings,json=disableUpdateSettings,proto3" json:"disable_update_settings,omitempty"`
|
DisableUpdateSettings bool `protobuf:"varint,2,opt,name=disable_update_settings,json=disableUpdateSettings,proto3" json:"disable_update_settings,omitempty"`
|
||||||
|
DisableNetworks bool `protobuf:"varint,3,opt,name=disable_networks,json=disableNetworks,proto3" json:"disable_networks,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -5027,6 +5028,13 @@ func (x *GetFeaturesResponse) GetDisableUpdateSettings() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *GetFeaturesResponse) GetDisableNetworks() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.DisableNetworks
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type TriggerUpdateRequest struct {
|
type TriggerUpdateRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
@@ -6472,10 +6480,11 @@ const file_daemon_proto_rawDesc = "" +
|
|||||||
"\f_profileNameB\v\n" +
|
"\f_profileNameB\v\n" +
|
||||||
"\t_username\"\x10\n" +
|
"\t_username\"\x10\n" +
|
||||||
"\x0eLogoutResponse\"\x14\n" +
|
"\x0eLogoutResponse\"\x14\n" +
|
||||||
"\x12GetFeaturesRequest\"x\n" +
|
"\x12GetFeaturesRequest\"\xa3\x01\n" +
|
||||||
"\x13GetFeaturesResponse\x12)\n" +
|
"\x13GetFeaturesResponse\x12)\n" +
|
||||||
"\x10disable_profiles\x18\x01 \x01(\bR\x0fdisableProfiles\x126\n" +
|
"\x10disable_profiles\x18\x01 \x01(\bR\x0fdisableProfiles\x126\n" +
|
||||||
"\x17disable_update_settings\x18\x02 \x01(\bR\x15disableUpdateSettings\"\x16\n" +
|
"\x17disable_update_settings\x18\x02 \x01(\bR\x15disableUpdateSettings\x12)\n" +
|
||||||
|
"\x10disable_networks\x18\x03 \x01(\bR\x0fdisableNetworks\"\x16\n" +
|
||||||
"\x14TriggerUpdateRequest\"M\n" +
|
"\x14TriggerUpdateRequest\"M\n" +
|
||||||
"\x15TriggerUpdateResponse\x12\x18\n" +
|
"\x15TriggerUpdateResponse\x12\x18\n" +
|
||||||
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1a\n" +
|
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1a\n" +
|
||||||
|
|||||||
@@ -727,6 +727,7 @@ message GetFeaturesRequest{}
|
|||||||
message GetFeaturesResponse{
|
message GetFeaturesResponse{
|
||||||
bool disable_profiles = 1;
|
bool disable_profiles = 1;
|
||||||
bool disable_update_settings = 2;
|
bool disable_update_settings = 2;
|
||||||
|
bool disable_networks = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message TriggerUpdateRequest {}
|
message TriggerUpdateRequest {}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/exp/maps"
|
"golang.org/x/exp/maps"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
gstatus "google.golang.org/grpc/status"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/proto"
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
"github.com/netbirdio/netbird/route"
|
"github.com/netbirdio/netbird/route"
|
||||||
@@ -27,6 +29,10 @@ func (s *Server) ListNetworks(context.Context, *proto.ListNetworksRequest) (*pro
|
|||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if s.networksDisabled {
|
||||||
|
return nil, gstatus.Errorf(codes.Unavailable, errNetworksDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
if s.connectClient == nil {
|
if s.connectClient == nil {
|
||||||
return nil, fmt.Errorf("not connected")
|
return nil, fmt.Errorf("not connected")
|
||||||
}
|
}
|
||||||
@@ -118,6 +124,10 @@ func (s *Server) SelectNetworks(_ context.Context, req *proto.SelectNetworksRequ
|
|||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if s.networksDisabled {
|
||||||
|
return nil, gstatus.Errorf(codes.Unavailable, errNetworksDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
if s.connectClient == nil {
|
if s.connectClient == nil {
|
||||||
return nil, fmt.Errorf("not connected")
|
return nil, fmt.Errorf("not connected")
|
||||||
}
|
}
|
||||||
@@ -164,6 +174,10 @@ func (s *Server) DeselectNetworks(_ context.Context, req *proto.SelectNetworksRe
|
|||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
defer s.mutex.Unlock()
|
defer s.mutex.Unlock()
|
||||||
|
|
||||||
|
if s.networksDisabled {
|
||||||
|
return nil, gstatus.Errorf(codes.Unavailable, errNetworksDisabled)
|
||||||
|
}
|
||||||
|
|
||||||
if s.connectClient == nil {
|
if s.connectClient == nil {
|
||||||
return nil, fmt.Errorf("not connected")
|
return nil, fmt.Errorf("not connected")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const (
|
|||||||
errRestoreResidualState = "failed to restore residual state: %v"
|
errRestoreResidualState = "failed to restore residual state: %v"
|
||||||
errProfilesDisabled = "profiles are disabled, you cannot use this feature without profiles enabled"
|
errProfilesDisabled = "profiles are disabled, you cannot use this feature without profiles enabled"
|
||||||
errUpdateSettingsDisabled = "update settings are disabled, you cannot use this feature without update settings enabled"
|
errUpdateSettingsDisabled = "update settings are disabled, you cannot use this feature without update settings enabled"
|
||||||
|
errNetworksDisabled = "network selection is disabled by the administrator"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrServiceNotUp = errors.New("service is not up")
|
var ErrServiceNotUp = errors.New("service is not up")
|
||||||
@@ -88,6 +89,7 @@ type Server struct {
|
|||||||
profileManager *profilemanager.ServiceManager
|
profileManager *profilemanager.ServiceManager
|
||||||
profilesDisabled bool
|
profilesDisabled bool
|
||||||
updateSettingsDisabled bool
|
updateSettingsDisabled bool
|
||||||
|
networksDisabled bool
|
||||||
|
|
||||||
sleepHandler *sleephandler.SleepHandler
|
sleepHandler *sleephandler.SleepHandler
|
||||||
|
|
||||||
@@ -104,7 +106,7 @@ type oauthAuthFlow struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New server instance constructor.
|
// New server instance constructor.
|
||||||
func New(ctx context.Context, logFile string, configFile string, profilesDisabled bool, updateSettingsDisabled bool) *Server {
|
func New(ctx context.Context, logFile string, configFile string, profilesDisabled bool, updateSettingsDisabled bool, networksDisabled bool) *Server {
|
||||||
s := &Server{
|
s := &Server{
|
||||||
rootCtx: ctx,
|
rootCtx: ctx,
|
||||||
logFile: logFile,
|
logFile: logFile,
|
||||||
@@ -113,6 +115,7 @@ func New(ctx context.Context, logFile string, configFile string, profilesDisable
|
|||||||
profileManager: profilemanager.NewServiceManager(configFile),
|
profileManager: profilemanager.NewServiceManager(configFile),
|
||||||
profilesDisabled: profilesDisabled,
|
profilesDisabled: profilesDisabled,
|
||||||
updateSettingsDisabled: updateSettingsDisabled,
|
updateSettingsDisabled: updateSettingsDisabled,
|
||||||
|
networksDisabled: networksDisabled,
|
||||||
jwtCache: newJWTCache(),
|
jwtCache: newJWTCache(),
|
||||||
}
|
}
|
||||||
agent := &serverAgent{s}
|
agent := &serverAgent{s}
|
||||||
@@ -1359,6 +1362,10 @@ func (s *Server) ExposeService(req *proto.ExposeServiceRequest, srv proto.Daemon
|
|||||||
return gstatus.Errorf(codes.FailedPrecondition, "engine not initialized")
|
return gstatus.Errorf(codes.FailedPrecondition, "engine not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if engine.IsBlockInbound() {
|
||||||
|
return gstatus.Errorf(codes.FailedPrecondition, "expose requires inbound connections but 'block inbound' is enabled, disable it first")
|
||||||
|
}
|
||||||
|
|
||||||
mgr := engine.GetExposeManager()
|
mgr := engine.GetExposeManager()
|
||||||
if mgr == nil {
|
if mgr == nil {
|
||||||
return gstatus.Errorf(codes.Internal, "expose manager not available")
|
return gstatus.Errorf(codes.Internal, "expose manager not available")
|
||||||
@@ -1624,6 +1631,7 @@ func (s *Server) GetFeatures(ctx context.Context, msg *proto.GetFeaturesRequest)
|
|||||||
features := &proto.GetFeaturesResponse{
|
features := &proto.GetFeaturesResponse{
|
||||||
DisableProfiles: s.checkProfilesDisabled(),
|
DisableProfiles: s.checkProfilesDisabled(),
|
||||||
DisableUpdateSettings: s.checkUpdateSettingsDisabled(),
|
DisableUpdateSettings: s.checkUpdateSettingsDisabled(),
|
||||||
|
DisableNetworks: s.networksDisabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
return features, nil
|
return features, nil
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func TestConnectWithRetryRuns(t *testing.T) {
|
|||||||
t.Fatalf("failed to set active profile state: %v", err)
|
t.Fatalf("failed to set active profile state: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s := New(ctx, "debug", "", false, false)
|
s := New(ctx, "debug", "", false, false, false)
|
||||||
|
|
||||||
s.config = config
|
s.config = config
|
||||||
|
|
||||||
@@ -164,7 +164,7 @@ func TestServer_Up(t *testing.T) {
|
|||||||
t.Fatalf("failed to set active profile state: %v", err)
|
t.Fatalf("failed to set active profile state: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s := New(ctx, "console", "", false, false)
|
s := New(ctx, "console", "", false, false, false)
|
||||||
err = s.Start()
|
err = s.Start()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ func TestServer_SubcribeEvents(t *testing.T) {
|
|||||||
t.Fatalf("failed to set active profile state: %v", err)
|
t.Fatalf("failed to set active profile state: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s := New(ctx, "console", "", false, false)
|
s := New(ctx, "console", "", false, false, false)
|
||||||
|
|
||||||
err = s.Start()
|
err = s.Start()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func TestSetConfig_AllFieldsSaved(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
s := New(ctx, "console", "", false, false)
|
s := New(ctx, "console", "", false, false, false)
|
||||||
|
|
||||||
rosenpassEnabled := true
|
rosenpassEnabled := true
|
||||||
rosenpassPermissive := true
|
rosenpassPermissive := true
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/ssh/config"
|
"github.com/netbirdio/netbird/client/ssh/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// registerStates registers all states that need crash recovery cleanup.
|
||||||
func registerStates(mgr *statemanager.Manager) {
|
func registerStates(mgr *statemanager.Manager) {
|
||||||
mgr.RegisterState(&dns.ShutdownState{})
|
mgr.RegisterState(&dns.ShutdownState{})
|
||||||
mgr.RegisterState(&systemops.ShutdownState{})
|
mgr.RegisterState(&systemops.ShutdownState{})
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/ssh/config"
|
"github.com/netbirdio/netbird/client/ssh/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// registerStates registers all states that need crash recovery cleanup.
|
||||||
func registerStates(mgr *statemanager.Manager) {
|
func registerStates(mgr *statemanager.Manager) {
|
||||||
mgr.RegisterState(&dns.ShutdownState{})
|
mgr.RegisterState(&dns.ShutdownState{})
|
||||||
mgr.RegisterState(&systemops.ShutdownState{})
|
mgr.RegisterState(&systemops.ShutdownState{})
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ func (p *SSHProxy) runProxySSHServer(jwtToken string) error {
|
|||||||
|
|
||||||
func (p *SSHProxy) handleSSHSession(session ssh.Session) {
|
func (p *SSHProxy) handleSSHSession(session ssh.Session) {
|
||||||
ptyReq, winCh, isPty := session.Pty()
|
ptyReq, winCh, isPty := session.Pty()
|
||||||
hasCommand := len(session.Command()) > 0
|
hasCommand := session.RawCommand() != ""
|
||||||
|
|
||||||
sshClient, err := p.getOrCreateBackendClient(session.Context(), session.User())
|
sshClient, err := p.getOrCreateBackendClient(session.Context(), session.User())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -180,7 +180,7 @@ func (p *SSHProxy) handleSSHSession(session ssh.Session) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasCommand {
|
if hasCommand {
|
||||||
if err := serverSession.Run(strings.Join(session.Command(), " ")); err != nil {
|
if err := serverSession.Run(session.RawCommand()); err != nil {
|
||||||
log.Debugf("run command: %v", err)
|
log.Debugf("run command: %v", err)
|
||||||
p.handleProxyExitCode(session, err)
|
p.handleProxyExitCode(session, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
@@ -245,6 +246,191 @@ func TestSSHProxy_Connect(t *testing.T) {
|
|||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSSHProxy_CommandQuoting verifies that the proxy preserves shell quoting
|
||||||
|
// when forwarding commands to the backend. This is critical for tools like
|
||||||
|
// Ansible that send commands such as:
|
||||||
|
//
|
||||||
|
// /bin/sh -c '( umask 77 && mkdir -p ... ) && sleep 0'
|
||||||
|
//
|
||||||
|
// The single quotes must be preserved so the backend shell receives the
|
||||||
|
// subshell expression as a single argument to -c.
|
||||||
|
func TestSSHProxy_CommandQuoting(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping integration test in short mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
sshClient, cleanup := setupProxySSHClient(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// These commands simulate what the SSH protocol delivers as exec payloads.
|
||||||
|
// When a user types: ssh host '/bin/sh -c "( echo hello )"'
|
||||||
|
// the local shell strips the outer single quotes, and the SSH exec request
|
||||||
|
// contains the raw string: /bin/sh -c "( echo hello )"
|
||||||
|
//
|
||||||
|
// The proxy must forward this string verbatim. Using session.Command()
|
||||||
|
// (shlex.Split + strings.Join) strips the inner double quotes, breaking
|
||||||
|
// the command on the backend.
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
command string
|
||||||
|
expect string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "subshell_in_double_quotes",
|
||||||
|
command: `/bin/sh -c "( echo from-subshell ) && echo outer"`,
|
||||||
|
expect: "from-subshell\nouter\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "printf_with_special_chars",
|
||||||
|
command: `/bin/sh -c "printf '%s\n' 'hello world'"`,
|
||||||
|
expect: "hello world\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested_command_substitution",
|
||||||
|
command: `/bin/sh -c "echo $(echo nested)"`,
|
||||||
|
expect: "nested\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
session, err := sshClient.NewSession()
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = session.Close() }()
|
||||||
|
|
||||||
|
var stderrBuf bytes.Buffer
|
||||||
|
session.Stderr = &stderrBuf
|
||||||
|
|
||||||
|
outputCh := make(chan []byte, 1)
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
output, err := session.Output(tc.command)
|
||||||
|
outputCh <- output
|
||||||
|
errCh <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case output := <-outputCh:
|
||||||
|
err := <-errCh
|
||||||
|
if stderrBuf.Len() > 0 {
|
||||||
|
t.Logf("stderr: %s", stderrBuf.String())
|
||||||
|
}
|
||||||
|
require.NoError(t, err, "command should succeed: %s", tc.command)
|
||||||
|
assert.Equal(t, tc.expect, string(output), "output mismatch for: %s", tc.command)
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatalf("command timed out: %s", tc.command)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupProxySSHClient creates a full proxy test environment and returns
|
||||||
|
// an SSH client connected through the proxy to a backend NetBird SSH server.
|
||||||
|
func setupProxySSHClient(t *testing.T) (*cryptossh.Client, func()) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
const (
|
||||||
|
issuer = "https://test-issuer.example.com"
|
||||||
|
audience = "test-audience"
|
||||||
|
)
|
||||||
|
|
||||||
|
jwksServer, privateKey, jwksURL := setupJWKSServer(t)
|
||||||
|
|
||||||
|
hostKey, err := nbssh.GeneratePrivateKey(nbssh.ED25519)
|
||||||
|
require.NoError(t, err)
|
||||||
|
hostPubKey, err := nbssh.GeneratePublicKey(hostKey)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
serverConfig := &server.Config{
|
||||||
|
HostKeyPEM: hostKey,
|
||||||
|
JWT: &server.JWTConfig{
|
||||||
|
Issuer: issuer,
|
||||||
|
Audiences: []string{audience},
|
||||||
|
KeysLocation: jwksURL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sshServer := server.New(serverConfig)
|
||||||
|
sshServer.SetAllowRootLogin(true)
|
||||||
|
|
||||||
|
testUsername := testutil.GetTestUsername(t)
|
||||||
|
testJWTUser := "test-username"
|
||||||
|
testUserHash, err := sshuserhash.HashUserID(testJWTUser)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
authConfig := &sshauth.Config{
|
||||||
|
UserIDClaim: sshauth.DefaultUserIDClaim,
|
||||||
|
AuthorizedUsers: []sshuserhash.UserIDHash{testUserHash},
|
||||||
|
MachineUsers: map[string][]uint32{
|
||||||
|
testUsername: {0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sshServer.UpdateSSHAuth(authConfig)
|
||||||
|
|
||||||
|
sshServerAddr := server.StartTestServer(t, sshServer)
|
||||||
|
|
||||||
|
mockDaemon := startMockDaemon(t)
|
||||||
|
|
||||||
|
host, portStr, err := net.SplitHostPort(sshServerAddr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mockDaemon.setHostKey(host, hostPubKey)
|
||||||
|
|
||||||
|
validToken := generateValidJWT(t, privateKey, issuer, audience, testJWTUser)
|
||||||
|
mockDaemon.setJWTToken(validToken)
|
||||||
|
|
||||||
|
proxyInstance, err := New(mockDaemon.addr, host, port, io.Discard, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
origStdin := os.Stdin
|
||||||
|
origStdout := os.Stdout
|
||||||
|
|
||||||
|
stdinReader, stdinWriter, err := os.Pipe()
|
||||||
|
require.NoError(t, err)
|
||||||
|
stdoutReader, stdoutWriter, err := os.Pipe()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
os.Stdin = stdinReader
|
||||||
|
os.Stdout = stdoutWriter
|
||||||
|
|
||||||
|
clientConn, proxyConn := net.Pipe()
|
||||||
|
|
||||||
|
go func() { _, _ = io.Copy(stdinWriter, proxyConn) }()
|
||||||
|
go func() { _, _ = io.Copy(proxyConn, stdoutReader) }()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = proxyInstance.Connect(ctx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
sshConfig := &cryptossh.ClientConfig{
|
||||||
|
User: testutil.GetTestUsername(t),
|
||||||
|
Auth: []cryptossh.AuthMethod{},
|
||||||
|
HostKeyCallback: cryptossh.InsecureIgnoreHostKey(),
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
sshClientConn, chans, reqs, err := cryptossh.NewClientConn(clientConn, "test", sshConfig)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client := cryptossh.NewClient(sshClientConn, chans, reqs)
|
||||||
|
|
||||||
|
cleanupFn := func() {
|
||||||
|
_ = client.Close()
|
||||||
|
_ = clientConn.Close()
|
||||||
|
cancel()
|
||||||
|
os.Stdin = origStdin
|
||||||
|
os.Stdout = origStdout
|
||||||
|
_ = sshServer.Stop()
|
||||||
|
mockDaemon.stop()
|
||||||
|
jwksServer.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, cleanupFn
|
||||||
|
}
|
||||||
|
|
||||||
type mockDaemonServer struct {
|
type mockDaemonServer struct {
|
||||||
proto.UnimplementedDaemonServiceServer
|
proto.UnimplementedDaemonServiceServer
|
||||||
hostKeys map[string][]byte
|
hostKeys map[string][]byte
|
||||||
|
|||||||
@@ -284,19 +284,21 @@ func (s *Server) closeListener(ln net.Listener) {
|
|||||||
// Stop closes the SSH server
|
// Stop closes the SSH server
|
||||||
func (s *Server) Stop() error {
|
func (s *Server) Stop() error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
sshServer := s.sshServer
|
||||||
|
if sshServer == nil {
|
||||||
if s.sshServer == nil {
|
s.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
s.sshServer = nil
|
||||||
|
s.listener = nil
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
if err := s.sshServer.Close(); err != nil {
|
// Close outside the lock: session handlers need s.mu for unregisterSession.
|
||||||
|
if err := sshServer.Close(); err != nil {
|
||||||
log.Debugf("close SSH server: %v", err)
|
log.Debugf("close SSH server: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.sshServer = nil
|
s.mu.Lock()
|
||||||
s.listener = nil
|
|
||||||
|
|
||||||
maps.Clear(s.sessions)
|
maps.Clear(s.sessions)
|
||||||
maps.Clear(s.pendingAuthJWT)
|
maps.Clear(s.pendingAuthJWT)
|
||||||
maps.Clear(s.connections)
|
maps.Clear(s.connections)
|
||||||
@@ -307,6 +309,7 @@ func (s *Server) Stop() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
maps.Clear(s.remoteForwardListeners)
|
maps.Clear(s.remoteForwardListeners)
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func (s *Server) sessionHandler(session ssh.Session) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ptyReq, winCh, isPty := session.Pty()
|
ptyReq, winCh, isPty := session.Pty()
|
||||||
hasCommand := len(session.Command()) > 0
|
hasCommand := session.RawCommand() != ""
|
||||||
|
|
||||||
if isPty && !hasCommand {
|
if isPty && !hasCommand {
|
||||||
// ssh <host> - PTY interactive session (login)
|
// ssh <host> - PTY interactive session (login)
|
||||||
|
|||||||
@@ -153,6 +153,9 @@ func networkAddresses() ([]NetworkAddress, error) {
|
|||||||
|
|
||||||
var netAddresses []NetworkAddress
|
var netAddresses []NetworkAddress
|
||||||
for _, iface := range interfaces {
|
for _, iface := range interfaces {
|
||||||
|
if iface.Flags&net.FlagUp == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if iface.HardwareAddr.String() == "" {
|
if iface.HardwareAddr.String() == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,18 +43,24 @@ func GetInfo(ctx context.Context) *Info {
|
|||||||
|
|
||||||
systemHostname, _ := os.Hostname()
|
systemHostname, _ := os.Hostname()
|
||||||
|
|
||||||
|
addrs, err := networkAddresses()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to discover network addresses: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
return &Info{
|
return &Info{
|
||||||
GoOS: runtime.GOOS,
|
GoOS: runtime.GOOS,
|
||||||
Kernel: osInfo[0],
|
Kernel: osInfo[0],
|
||||||
Platform: runtime.GOARCH,
|
Platform: runtime.GOARCH,
|
||||||
OS: osName,
|
OS: osName,
|
||||||
OSVersion: osVersion,
|
OSVersion: osVersion,
|
||||||
Hostname: extractDeviceName(ctx, systemHostname),
|
Hostname: extractDeviceName(ctx, systemHostname),
|
||||||
CPUs: runtime.NumCPU(),
|
CPUs: runtime.NumCPU(),
|
||||||
NetbirdVersion: version.NetbirdVersion(),
|
NetbirdVersion: version.NetbirdVersion(),
|
||||||
UIVersion: extractUserAgent(ctx),
|
UIVersion: extractUserAgent(ctx),
|
||||||
KernelVersion: osInfo[1],
|
KernelVersion: osInfo[1],
|
||||||
Environment: env,
|
NetworkAddresses: addrs,
|
||||||
|
Environment: env,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UpdateStaticInfoAsync is a no-op on Android as there is no static info to update
|
// UpdateStaticInfoAsync is a no-op on iOS as there is no static info to update
|
||||||
func UpdateStaticInfoAsync() {
|
func UpdateStaticInfoAsync() {
|
||||||
// do nothing
|
// do nothing
|
||||||
}
|
}
|
||||||
@@ -15,11 +17,24 @@ func UpdateStaticInfoAsync() {
|
|||||||
// GetInfo retrieves and parses the system information
|
// GetInfo retrieves and parses the system information
|
||||||
func GetInfo(ctx context.Context) *Info {
|
func GetInfo(ctx context.Context) *Info {
|
||||||
|
|
||||||
// Convert fixed-size byte arrays to Go strings
|
|
||||||
sysName := extractOsName(ctx, "sysName")
|
sysName := extractOsName(ctx, "sysName")
|
||||||
swVersion := extractOsVersion(ctx, "swVersion")
|
swVersion := extractOsVersion(ctx, "swVersion")
|
||||||
|
|
||||||
gio := &Info{Kernel: sysName, OSVersion: swVersion, Platform: "unknown", OS: sysName, GoOS: runtime.GOOS, CPUs: runtime.NumCPU(), KernelVersion: swVersion}
|
addrs, err := networkAddresses()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to discover network addresses: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gio := &Info{
|
||||||
|
Kernel: sysName,
|
||||||
|
OSVersion: swVersion,
|
||||||
|
Platform: "unknown",
|
||||||
|
OS: sysName,
|
||||||
|
GoOS: runtime.GOOS,
|
||||||
|
CPUs: runtime.NumCPU(),
|
||||||
|
KernelVersion: swVersion,
|
||||||
|
NetworkAddresses: addrs,
|
||||||
|
}
|
||||||
gio.Hostname = extractDeviceName(ctx, "hostname")
|
gio.Hostname = extractDeviceName(ctx, "hostname")
|
||||||
gio.NetbirdVersion = version.NetbirdVersion()
|
gio.NetbirdVersion = version.NetbirdVersion()
|
||||||
gio.UIVersion = extractUserAgent(ctx)
|
gio.UIVersion = extractUserAgent(ctx)
|
||||||
|
|||||||
@@ -314,6 +314,7 @@ type serviceClient struct {
|
|||||||
lastNotifiedVersion string
|
lastNotifiedVersion string
|
||||||
settingsEnabled bool
|
settingsEnabled bool
|
||||||
profilesEnabled bool
|
profilesEnabled bool
|
||||||
|
networksEnabled bool
|
||||||
showNetworks bool
|
showNetworks bool
|
||||||
wNetworks fyne.Window
|
wNetworks fyne.Window
|
||||||
wProfiles fyne.Window
|
wProfiles fyne.Window
|
||||||
@@ -368,6 +369,7 @@ func newServiceClient(args *newServiceClientArgs) *serviceClient {
|
|||||||
|
|
||||||
showAdvancedSettings: args.showSettings,
|
showAdvancedSettings: args.showSettings,
|
||||||
showNetworks: args.showNetworks,
|
showNetworks: args.showNetworks,
|
||||||
|
networksEnabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.eventHandler = newEventHandler(s)
|
s.eventHandler = newEventHandler(s)
|
||||||
@@ -920,8 +922,10 @@ func (s *serviceClient) updateStatus() error {
|
|||||||
s.mStatus.SetIcon(s.icConnectedDot)
|
s.mStatus.SetIcon(s.icConnectedDot)
|
||||||
s.mUp.Disable()
|
s.mUp.Disable()
|
||||||
s.mDown.Enable()
|
s.mDown.Enable()
|
||||||
s.mNetworks.Enable()
|
if s.networksEnabled {
|
||||||
s.mExitNode.Enable()
|
s.mNetworks.Enable()
|
||||||
|
s.mExitNode.Enable()
|
||||||
|
}
|
||||||
s.startExitNodeRefresh()
|
s.startExitNodeRefresh()
|
||||||
systrayIconState = true
|
systrayIconState = true
|
||||||
case status.Status == string(internal.StatusConnecting):
|
case status.Status == string(internal.StatusConnecting):
|
||||||
@@ -1093,14 +1097,14 @@ func (s *serviceClient) onTrayReady() {
|
|||||||
s.getSrvConfig()
|
s.getSrvConfig()
|
||||||
time.Sleep(100 * time.Millisecond) // To prevent race condition caused by systray not being fully initialized and ignoring setIcon
|
time.Sleep(100 * time.Millisecond) // To prevent race condition caused by systray not being fully initialized and ignoring setIcon
|
||||||
for {
|
for {
|
||||||
|
// Check features before status so menus respect disable flags before being enabled
|
||||||
|
s.checkAndUpdateFeatures()
|
||||||
|
|
||||||
err := s.updateStatus()
|
err := s.updateStatus()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error while updating status: %v", err)
|
log.Errorf("error while updating status: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check features periodically to handle daemon restarts
|
|
||||||
s.checkAndUpdateFeatures()
|
|
||||||
|
|
||||||
time.Sleep(2 * time.Second)
|
time.Sleep(2 * time.Second)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -1299,6 +1303,16 @@ func (s *serviceClient) checkAndUpdateFeatures() {
|
|||||||
s.mProfile.setEnabled(profilesEnabled)
|
s.mProfile.setEnabled(profilesEnabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update networks and exit node menus based on current features
|
||||||
|
s.networksEnabled = features == nil || !features.DisableNetworks
|
||||||
|
if s.networksEnabled && s.connected {
|
||||||
|
s.mNetworks.Enable()
|
||||||
|
s.mExitNode.Enable()
|
||||||
|
} else {
|
||||||
|
s.mNetworks.Disable()
|
||||||
|
s.mExitNode.Disable()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFeatures from the daemon to determine which features are enabled/disabled.
|
// getFeatures from the daemon to determine which features are enabled/disabled.
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ import (
|
|||||||
|
|
||||||
// Initial state for the debug collection
|
// Initial state for the debug collection
|
||||||
type debugInitialState struct {
|
type debugInitialState struct {
|
||||||
wasDown bool
|
wasDown bool
|
||||||
logLevel proto.LogLevel
|
needsRestoreUp bool
|
||||||
isLevelTrace bool
|
logLevel proto.LogLevel
|
||||||
|
isLevelTrace bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug collection parameters
|
// Debug collection parameters
|
||||||
@@ -371,46 +372,51 @@ func (s *serviceClient) configureServiceForDebug(
|
|||||||
conn proto.DaemonServiceClient,
|
conn proto.DaemonServiceClient,
|
||||||
state *debugInitialState,
|
state *debugInitialState,
|
||||||
enablePersistence bool,
|
enablePersistence bool,
|
||||||
) error {
|
) {
|
||||||
if state.wasDown {
|
if state.wasDown {
|
||||||
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
||||||
return fmt.Errorf("bring service up: %v", err)
|
log.Warnf("failed to bring service up: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Info("Service brought up for debug")
|
||||||
|
time.Sleep(time.Second * 10)
|
||||||
}
|
}
|
||||||
log.Info("Service brought up for debug")
|
|
||||||
time.Sleep(time.Second * 10)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !state.isLevelTrace {
|
if !state.isLevelTrace {
|
||||||
if _, err := conn.SetLogLevel(s.ctx, &proto.SetLogLevelRequest{Level: proto.LogLevel_TRACE}); err != nil {
|
if _, err := conn.SetLogLevel(s.ctx, &proto.SetLogLevelRequest{Level: proto.LogLevel_TRACE}); err != nil {
|
||||||
return fmt.Errorf("set log level to TRACE: %v", err)
|
log.Warnf("failed to set log level to TRACE: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Info("Log level set to TRACE for debug")
|
||||||
}
|
}
|
||||||
log.Info("Log level set to TRACE for debug")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil {
|
if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil {
|
||||||
return fmt.Errorf("bring service down: %v", err)
|
log.Warnf("failed to bring service down: %v", err)
|
||||||
|
} else {
|
||||||
|
state.needsRestoreUp = !state.wasDown
|
||||||
|
time.Sleep(time.Second)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second)
|
|
||||||
|
|
||||||
if enablePersistence {
|
if enablePersistence {
|
||||||
if _, err := conn.SetSyncResponsePersistence(s.ctx, &proto.SetSyncResponsePersistenceRequest{
|
if _, err := conn.SetSyncResponsePersistence(s.ctx, &proto.SetSyncResponsePersistenceRequest{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("enable sync response persistence: %v", err)
|
log.Warnf("failed to enable sync response persistence: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Info("Sync response persistence enabled for debug")
|
||||||
}
|
}
|
||||||
log.Info("Sync response persistence enabled for debug")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
||||||
return fmt.Errorf("bring service back up: %v", err)
|
log.Warnf("failed to bring service back up: %v", err)
|
||||||
|
} else {
|
||||||
|
state.needsRestoreUp = false
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
}
|
}
|
||||||
time.Sleep(time.Second * 3)
|
|
||||||
|
|
||||||
if _, err := conn.StartCPUProfile(s.ctx, &proto.StartCPUProfileRequest{}); err != nil {
|
if _, err := conn.StartCPUProfile(s.ctx, &proto.StartCPUProfileRequest{}); err != nil {
|
||||||
log.Warnf("failed to start CPU profiling: %v", err)
|
log.Warnf("failed to start CPU profiling: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serviceClient) collectDebugData(
|
func (s *serviceClient) collectDebugData(
|
||||||
@@ -424,9 +430,7 @@ func (s *serviceClient) collectDebugData(
|
|||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
startProgressTracker(ctx, &wg, params.duration, progress)
|
startProgressTracker(ctx, &wg, params.duration, progress)
|
||||||
|
|
||||||
if err := s.configureServiceForDebug(conn, state, params.enablePersistence); err != nil {
|
s.configureServiceForDebug(conn, state, params.enablePersistence)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
progress.progressBar.Hide()
|
progress.progressBar.Hide()
|
||||||
@@ -482,9 +486,17 @@ func (s *serviceClient) createDebugBundleFromCollection(
|
|||||||
|
|
||||||
// Restore service to original state
|
// Restore service to original state
|
||||||
func (s *serviceClient) restoreServiceState(conn proto.DaemonServiceClient, state *debugInitialState) {
|
func (s *serviceClient) restoreServiceState(conn proto.DaemonServiceClient, state *debugInitialState) {
|
||||||
|
if state.needsRestoreUp {
|
||||||
|
if _, err := conn.Up(s.ctx, &proto.UpRequest{}); err != nil {
|
||||||
|
log.Warnf("failed to restore up state: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Info("Service state restored to up")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if state.wasDown {
|
if state.wasDown {
|
||||||
if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil {
|
if _, err := conn.Down(s.ctx, &proto.DownRequest{}); err != nil {
|
||||||
log.Errorf("Failed to restore down state: %v", err)
|
log.Warnf("failed to restore down state: %v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Info("Service state restored to down")
|
log.Info("Service state restored to down")
|
||||||
}
|
}
|
||||||
@@ -492,7 +504,7 @@ func (s *serviceClient) restoreServiceState(conn proto.DaemonServiceClient, stat
|
|||||||
|
|
||||||
if !state.isLevelTrace {
|
if !state.isLevelTrace {
|
||||||
if _, err := conn.SetLogLevel(s.ctx, &proto.SetLogLevelRequest{Level: state.logLevel}); err != nil {
|
if _, err := conn.SetLogLevel(s.ctx, &proto.SetLogLevelRequest{Level: state.logLevel}); err != nil {
|
||||||
log.Errorf("Failed to restore log level: %v", err)
|
log.Warnf("failed to restore log level: %v", err)
|
||||||
} else {
|
} else {
|
||||||
log.Info("Log level restored to original setting")
|
log.Info("Log level restored to original setting")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,9 +179,11 @@ type StoreConfig struct {
|
|||||||
|
|
||||||
// ReverseProxyConfig contains reverse proxy settings
|
// ReverseProxyConfig contains reverse proxy settings
|
||||||
type ReverseProxyConfig struct {
|
type ReverseProxyConfig struct {
|
||||||
TrustedHTTPProxies []string `yaml:"trustedHTTPProxies"`
|
TrustedHTTPProxies []string `yaml:"trustedHTTPProxies"`
|
||||||
TrustedHTTPProxiesCount uint `yaml:"trustedHTTPProxiesCount"`
|
TrustedHTTPProxiesCount uint `yaml:"trustedHTTPProxiesCount"`
|
||||||
TrustedPeers []string `yaml:"trustedPeers"`
|
TrustedPeers []string `yaml:"trustedPeers"`
|
||||||
|
AccessLogRetentionDays int `yaml:"accessLogRetentionDays"`
|
||||||
|
AccessLogCleanupIntervalHours int `yaml:"accessLogCleanupIntervalHours"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfig returns a CombinedConfig with default values
|
// DefaultConfig returns a CombinedConfig with default values
|
||||||
@@ -645,7 +647,9 @@ func (c *CombinedConfig) ToManagementConfig() (*nbconfig.Config, error) {
|
|||||||
|
|
||||||
// Build reverse proxy config
|
// Build reverse proxy config
|
||||||
reverseProxy := nbconfig.ReverseProxy{
|
reverseProxy := nbconfig.ReverseProxy{
|
||||||
TrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,
|
TrustedHTTPProxiesCount: mgmt.ReverseProxy.TrustedHTTPProxiesCount,
|
||||||
|
AccessLogRetentionDays: mgmt.ReverseProxy.AccessLogRetentionDays,
|
||||||
|
AccessLogCleanupIntervalHours: mgmt.ReverseProxy.AccessLogCleanupIntervalHours,
|
||||||
}
|
}
|
||||||
for _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {
|
for _, p := range mgmt.ReverseProxy.TrustedHTTPProxies {
|
||||||
if prefix, err := netip.ParsePrefix(p); err == nil {
|
if prefix, err := netip.ParsePrefix(p); err == nil {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import (
|
|||||||
"github.com/netbirdio/netbird/management/server/telemetry"
|
"github.com/netbirdio/netbird/management/server/telemetry"
|
||||||
"github.com/netbirdio/netbird/relay/healthcheck"
|
"github.com/netbirdio/netbird/relay/healthcheck"
|
||||||
relayServer "github.com/netbirdio/netbird/relay/server"
|
relayServer "github.com/netbirdio/netbird/relay/server"
|
||||||
|
"github.com/netbirdio/netbird/relay/server/listener"
|
||||||
"github.com/netbirdio/netbird/relay/server/listener/ws"
|
"github.com/netbirdio/netbird/relay/server/listener/ws"
|
||||||
sharedMetrics "github.com/netbirdio/netbird/shared/metrics"
|
sharedMetrics "github.com/netbirdio/netbird/shared/metrics"
|
||||||
"github.com/netbirdio/netbird/shared/relay/auth"
|
"github.com/netbirdio/netbird/shared/relay/auth"
|
||||||
@@ -523,7 +524,7 @@ func createManagementServer(cfg *CombinedConfig, mgmtConfig *nbconfig.Config) (*
|
|||||||
func createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, relaySrv *relayServer.Server, meter metric.Meter, cfg *CombinedConfig) http.Handler {
|
func createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, relaySrv *relayServer.Server, meter metric.Meter, cfg *CombinedConfig) http.Handler {
|
||||||
wsProxy := wsproxyserver.New(grpcServer, wsproxyserver.WithOTelMeter(meter))
|
wsProxy := wsproxyserver.New(grpcServer, wsproxyserver.WithOTelMeter(meter))
|
||||||
|
|
||||||
var relayAcceptFn func(conn net.Conn)
|
var relayAcceptFn func(conn listener.Conn)
|
||||||
if relaySrv != nil {
|
if relaySrv != nil {
|
||||||
relayAcceptFn = relaySrv.RelayAccept()
|
relayAcceptFn = relaySrv.RelayAccept()
|
||||||
}
|
}
|
||||||
@@ -563,7 +564,7 @@ func createCombinedHandler(grpcServer *grpc.Server, httpHandler http.Handler, re
|
|||||||
}
|
}
|
||||||
|
|
||||||
// handleRelayWebSocket handles incoming WebSocket connections for the relay service
|
// handleRelayWebSocket handles incoming WebSocket connections for the relay service
|
||||||
func handleRelayWebSocket(w http.ResponseWriter, r *http.Request, acceptFn func(conn net.Conn), cfg *CombinedConfig) {
|
func handleRelayWebSocket(w http.ResponseWriter, r *http.Request, acceptFn func(conn listener.Conn), cfg *CombinedConfig) {
|
||||||
acceptOptions := &websocket.AcceptOptions{
|
acceptOptions := &websocket.AcceptOptions{
|
||||||
OriginPatterns: []string{"*"},
|
OriginPatterns: []string{"*"},
|
||||||
}
|
}
|
||||||
@@ -585,15 +586,9 @@ func handleRelayWebSocket(w http.ResponseWriter, r *http.Request, acceptFn func(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
lAddr, err := net.ResolveTCPAddr("tcp", cfg.Server.ListenAddress)
|
|
||||||
if err != nil {
|
|
||||||
_ = wsConn.Close(websocket.StatusInternalError, "internal error")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("Relay WS client connected from: %s", rAddr)
|
log.Debugf("Relay WS client connected from: %s", rAddr)
|
||||||
|
|
||||||
conn := ws.NewConn(wsConn, lAddr, rAddr)
|
conn := ws.NewConn(wsConn, rAddr)
|
||||||
acceptFn(conn)
|
acceptFn(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/connectivity"
|
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
"google.golang.org/grpc/keepalive"
|
"google.golang.org/grpc/keepalive"
|
||||||
@@ -26,11 +25,22 @@ import (
|
|||||||
"github.com/netbirdio/netbird/util/wsproxy"
|
"github.com/netbirdio/netbird/util/wsproxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrClientClosed = errors.New("client is closed")
|
||||||
|
|
||||||
|
// minHealthyDuration is the minimum time a stream must survive before a failure
|
||||||
|
// resets the backoff timer. Streams that fail faster are considered unhealthy and
|
||||||
|
// should not reset backoff, so that MaxElapsedTime can eventually stop retries.
|
||||||
|
const minHealthyDuration = 5 * time.Second
|
||||||
|
|
||||||
type GRPCClient struct {
|
type GRPCClient struct {
|
||||||
realClient proto.FlowServiceClient
|
realClient proto.FlowServiceClient
|
||||||
clientConn *grpc.ClientConn
|
clientConn *grpc.ClientConn
|
||||||
stream proto.FlowService_EventsClient
|
stream proto.FlowService_EventsClient
|
||||||
streamMu sync.Mutex
|
target string
|
||||||
|
opts []grpc.DialOption
|
||||||
|
closed bool // prevent creating conn in the middle of the Close
|
||||||
|
receiving bool // prevent concurrent Receive calls
|
||||||
|
mu sync.Mutex // protects clientConn, realClient, stream, closed, and receiving
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(addr, payload, signature string, interval time.Duration) (*GRPCClient, error) {
|
func NewClient(addr, payload, signature string, interval time.Duration) (*GRPCClient, error) {
|
||||||
@@ -65,7 +75,8 @@ func NewClient(addr, payload, signature string, interval time.Duration) (*GRPCCl
|
|||||||
grpc.WithDefaultServiceConfig(`{"healthCheckConfig": {"serviceName": ""}}`),
|
grpc.WithDefaultServiceConfig(`{"healthCheckConfig": {"serviceName": ""}}`),
|
||||||
)
|
)
|
||||||
|
|
||||||
conn, err := grpc.NewClient(fmt.Sprintf("%s:%s", parsedURL.Hostname(), parsedURL.Port()), opts...)
|
target := parsedURL.Host
|
||||||
|
conn, err := grpc.NewClient(target, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("creating new grpc client: %w", err)
|
return nil, fmt.Errorf("creating new grpc client: %w", err)
|
||||||
}
|
}
|
||||||
@@ -73,30 +84,73 @@ func NewClient(addr, payload, signature string, interval time.Duration) (*GRPCCl
|
|||||||
return &GRPCClient{
|
return &GRPCClient{
|
||||||
realClient: proto.NewFlowServiceClient(conn),
|
realClient: proto.NewFlowServiceClient(conn),
|
||||||
clientConn: conn,
|
clientConn: conn,
|
||||||
|
target: target,
|
||||||
|
opts: opts,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GRPCClient) Close() error {
|
func (c *GRPCClient) Close() error {
|
||||||
c.streamMu.Lock()
|
c.mu.Lock()
|
||||||
defer c.streamMu.Unlock()
|
c.closed = true
|
||||||
|
|
||||||
c.stream = nil
|
c.stream = nil
|
||||||
if err := c.clientConn.Close(); err != nil && !errors.Is(err, context.Canceled) {
|
conn := c.clientConn
|
||||||
|
c.clientConn = nil
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if conn == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := conn.Close(); err != nil && !errors.Is(err, context.Canceled) {
|
||||||
return fmt.Errorf("close client connection: %w", err)
|
return fmt.Errorf("close client connection: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) Send(event *proto.FlowEvent) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
stream := c.stream
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if stream == nil {
|
||||||
|
return errors.New("stream not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := stream.Send(event); err != nil {
|
||||||
|
return fmt.Errorf("send flow event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *GRPCClient) Receive(ctx context.Context, interval time.Duration, msgHandler func(msg *proto.FlowEventAck) error) error {
|
func (c *GRPCClient) Receive(ctx context.Context, interval time.Duration, msgHandler func(msg *proto.FlowEventAck) error) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.receiving {
|
||||||
|
c.mu.Unlock()
|
||||||
|
return errors.New("concurrent Receive calls are not supported")
|
||||||
|
}
|
||||||
|
c.receiving = true
|
||||||
|
c.mu.Unlock()
|
||||||
|
defer func() {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.receiving = false
|
||||||
|
c.mu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
backOff := defaultBackoff(ctx, interval)
|
backOff := defaultBackoff(ctx, interval)
|
||||||
operation := func() error {
|
operation := func() error {
|
||||||
if err := c.establishStreamAndReceive(ctx, msgHandler); err != nil {
|
stream, err := c.establishStream(ctx)
|
||||||
if s, ok := status.FromError(err); ok && s.Code() == codes.Canceled {
|
if err != nil {
|
||||||
return fmt.Errorf("receive: %w: %w", err, context.Canceled)
|
log.Errorf("failed to establish flow stream, retrying: %v", err)
|
||||||
}
|
return c.handleRetryableError(err, time.Time{}, backOff)
|
||||||
|
}
|
||||||
|
|
||||||
|
streamStart := time.Now()
|
||||||
|
|
||||||
|
if err := c.receive(stream, msgHandler); err != nil {
|
||||||
log.Errorf("receive failed: %v", err)
|
log.Errorf("receive failed: %v", err)
|
||||||
return fmt.Errorf("receive: %w", err)
|
return c.handleRetryableError(err, streamStart, backOff)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -108,37 +162,106 @@ func (c *GRPCClient) Receive(ctx context.Context, interval time.Duration, msgHan
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GRPCClient) establishStreamAndReceive(ctx context.Context, msgHandler func(msg *proto.FlowEventAck) error) error {
|
// handleRetryableError resets the backoff timer if the stream was healthy long
|
||||||
if c.clientConn.GetState() == connectivity.Shutdown {
|
// enough and recreates the underlying ClientConn so that gRPC's internal
|
||||||
return errors.New("connection to flow receiver has been shut down")
|
// subchannel backoff does not accumulate and compete with our own retry timer.
|
||||||
|
// A zero streamStart means the stream was never established.
|
||||||
|
func (c *GRPCClient) handleRetryableError(err error, streamStart time.Time, backOff backoff.BackOff) error {
|
||||||
|
if isContextDone(err) {
|
||||||
|
return backoff.Permanent(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
stream, err := c.realClient.Events(ctx, grpc.WaitForReady(true))
|
var permErr *backoff.PermanentError
|
||||||
if err != nil {
|
if errors.As(err, &permErr) {
|
||||||
return fmt.Errorf("create event stream: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = stream.Send(&proto.FlowEvent{IsInitiator: true})
|
// Reset the backoff so the next retry starts with a short delay instead of
|
||||||
|
// continuing the already-elapsed timer. Only do this if the stream was healthy
|
||||||
|
// long enough; short-lived connect/drop cycles must not defeat MaxElapsedTime.
|
||||||
|
if !streamStart.IsZero() && time.Since(streamStart) >= minHealthyDuration {
|
||||||
|
backOff.Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
if recreateErr := c.recreateConnection(); recreateErr != nil {
|
||||||
|
log.Errorf("recreate connection: %v", recreateErr)
|
||||||
|
return recreateErr
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("connection recreated, retrying stream")
|
||||||
|
return fmt.Errorf("retrying after error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) recreateConnection() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.closed {
|
||||||
|
c.mu.Unlock()
|
||||||
|
return backoff.Permanent(ErrClientClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := grpc.NewClient(c.target, c.opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Infof("failed to send initiator message to flow receiver but will attempt to continue. Error: %s", err)
|
c.mu.Unlock()
|
||||||
|
return fmt.Errorf("create new connection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
old := c.clientConn
|
||||||
|
c.clientConn = conn
|
||||||
|
c.realClient = proto.NewFlowServiceClient(conn)
|
||||||
|
c.stream = nil
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
_ = old.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) establishStream(ctx context.Context) (proto.FlowService_EventsClient, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.closed {
|
||||||
|
c.mu.Unlock()
|
||||||
|
return nil, backoff.Permanent(ErrClientClosed)
|
||||||
|
}
|
||||||
|
cl := c.realClient
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
// open stream outside the lock — blocking operation
|
||||||
|
stream, err := cl.Events(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create event stream: %w", err)
|
||||||
|
}
|
||||||
|
streamReady := false
|
||||||
|
defer func() {
|
||||||
|
if !streamReady {
|
||||||
|
_ = stream.CloseSend()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = stream.Send(&proto.FlowEvent{IsInitiator: true}); err != nil {
|
||||||
|
return nil, fmt.Errorf("send initiator: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = checkHeader(stream); err != nil {
|
if err = checkHeader(stream); err != nil {
|
||||||
return fmt.Errorf("check header: %w", err)
|
return nil, fmt.Errorf("check header: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.streamMu.Lock()
|
c.mu.Lock()
|
||||||
|
if c.closed {
|
||||||
|
c.mu.Unlock()
|
||||||
|
return nil, backoff.Permanent(ErrClientClosed)
|
||||||
|
}
|
||||||
c.stream = stream
|
c.stream = stream
|
||||||
c.streamMu.Unlock()
|
c.mu.Unlock()
|
||||||
|
streamReady = true
|
||||||
|
|
||||||
return c.receive(stream, msgHandler)
|
return stream, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GRPCClient) receive(stream proto.FlowService_EventsClient, msgHandler func(msg *proto.FlowEventAck) error) error {
|
func (c *GRPCClient) receive(stream proto.FlowService_EventsClient, msgHandler func(msg *proto.FlowEventAck) error) error {
|
||||||
for {
|
for {
|
||||||
msg, err := stream.Recv()
|
msg, err := stream.Recv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("receive from stream: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg.IsInitiator {
|
if msg.IsInitiator {
|
||||||
@@ -169,7 +292,7 @@ func checkHeader(stream proto.FlowService_EventsClient) error {
|
|||||||
func defaultBackoff(ctx context.Context, interval time.Duration) backoff.BackOff {
|
func defaultBackoff(ctx context.Context, interval time.Duration) backoff.BackOff {
|
||||||
return backoff.WithContext(&backoff.ExponentialBackOff{
|
return backoff.WithContext(&backoff.ExponentialBackOff{
|
||||||
InitialInterval: 800 * time.Millisecond,
|
InitialInterval: 800 * time.Millisecond,
|
||||||
RandomizationFactor: 1,
|
RandomizationFactor: 0.5,
|
||||||
Multiplier: 1.7,
|
Multiplier: 1.7,
|
||||||
MaxInterval: interval / 2,
|
MaxInterval: interval / 2,
|
||||||
MaxElapsedTime: 3 * 30 * 24 * time.Hour, // 3 months
|
MaxElapsedTime: 3 * 30 * 24 * time.Hour, // 3 months
|
||||||
@@ -178,18 +301,12 @@ func defaultBackoff(ctx context.Context, interval time.Duration) backoff.BackOff
|
|||||||
}, ctx)
|
}, ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *GRPCClient) Send(event *proto.FlowEvent) error {
|
func isContextDone(err error) bool {
|
||||||
c.streamMu.Lock()
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
stream := c.stream
|
return true
|
||||||
c.streamMu.Unlock()
|
|
||||||
|
|
||||||
if stream == nil {
|
|
||||||
return errors.New("stream not initialized")
|
|
||||||
}
|
}
|
||||||
|
if s, ok := status.FromError(err); ok {
|
||||||
if err := stream.Send(event); err != nil {
|
return s.Code() == codes.Canceled || s.Code() == codes.DeadlineExceeded
|
||||||
return fmt.Errorf("send flow event: %w", err)
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package client_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -11,6 +14,8 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
flow "github.com/netbirdio/netbird/flow/client"
|
flow "github.com/netbirdio/netbird/flow/client"
|
||||||
"github.com/netbirdio/netbird/flow/proto"
|
"github.com/netbirdio/netbird/flow/proto"
|
||||||
@@ -18,21 +23,89 @@ import (
|
|||||||
|
|
||||||
type testServer struct {
|
type testServer struct {
|
||||||
proto.UnimplementedFlowServiceServer
|
proto.UnimplementedFlowServiceServer
|
||||||
events chan *proto.FlowEvent
|
events chan *proto.FlowEvent
|
||||||
acks chan *proto.FlowEventAck
|
acks chan *proto.FlowEventAck
|
||||||
grpcSrv *grpc.Server
|
grpcSrv *grpc.Server
|
||||||
addr string
|
addr string
|
||||||
|
listener *connTrackListener
|
||||||
|
closeStream chan struct{} // signal server to close the stream
|
||||||
|
handlerDone chan struct{} // signaled each time Events() exits
|
||||||
|
handlerStarted chan struct{} // signaled each time Events() begins
|
||||||
|
}
|
||||||
|
|
||||||
|
// connTrackListener wraps a net.Listener to track accepted connections
|
||||||
|
// so tests can forcefully close them to simulate PROTOCOL_ERROR/RST_STREAM.
|
||||||
|
type connTrackListener struct {
|
||||||
|
net.Listener
|
||||||
|
mu sync.Mutex
|
||||||
|
conns []net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *connTrackListener) Accept() (net.Conn, error) {
|
||||||
|
c, err := l.Listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
l.mu.Lock()
|
||||||
|
l.conns = append(l.conns, c)
|
||||||
|
l.mu.Unlock()
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendRSTStream writes a raw HTTP/2 RST_STREAM frame with PROTOCOL_ERROR
|
||||||
|
// (error code 0x1) on every tracked connection. This produces the exact error:
|
||||||
|
//
|
||||||
|
// rpc error: code = Internal desc = stream terminated by RST_STREAM with error code: PROTOCOL_ERROR
|
||||||
|
//
|
||||||
|
// HTTP/2 RST_STREAM frame format (9-byte header + 4-byte payload):
|
||||||
|
//
|
||||||
|
// Length (3 bytes): 0x000004
|
||||||
|
// Type (1 byte): 0x03 (RST_STREAM)
|
||||||
|
// Flags (1 byte): 0x00
|
||||||
|
// Stream ID (4 bytes): target stream (must have bit 31 clear)
|
||||||
|
// Error Code (4 bytes): 0x00000001 (PROTOCOL_ERROR)
|
||||||
|
func (l *connTrackListener) connCount() int {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
return len(l.conns)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *connTrackListener) sendRSTStream(streamID uint32) {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
frame := make([]byte, 13) // 9-byte header + 4-byte payload
|
||||||
|
// Length = 4 (3 bytes, big-endian)
|
||||||
|
frame[0], frame[1], frame[2] = 0, 0, 4
|
||||||
|
// Type = RST_STREAM (0x03)
|
||||||
|
frame[3] = 0x03
|
||||||
|
// Flags = 0
|
||||||
|
frame[4] = 0x00
|
||||||
|
// Stream ID (4 bytes, big-endian, bit 31 reserved = 0)
|
||||||
|
binary.BigEndian.PutUint32(frame[5:9], streamID)
|
||||||
|
// Error Code = PROTOCOL_ERROR (0x1)
|
||||||
|
binary.BigEndian.PutUint32(frame[9:13], 0x1)
|
||||||
|
|
||||||
|
for _, c := range l.conns {
|
||||||
|
_, _ = c.Write(frame)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestServer(t *testing.T) *testServer {
|
func newTestServer(t *testing.T) *testServer {
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
rawListener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
listener := &connTrackListener{Listener: rawListener}
|
||||||
|
|
||||||
s := &testServer{
|
s := &testServer{
|
||||||
events: make(chan *proto.FlowEvent, 100),
|
events: make(chan *proto.FlowEvent, 100),
|
||||||
acks: make(chan *proto.FlowEventAck, 100),
|
acks: make(chan *proto.FlowEventAck, 100),
|
||||||
grpcSrv: grpc.NewServer(),
|
grpcSrv: grpc.NewServer(),
|
||||||
addr: listener.Addr().String(),
|
addr: rawListener.Addr().String(),
|
||||||
|
listener: listener,
|
||||||
|
closeStream: make(chan struct{}, 1),
|
||||||
|
handlerDone: make(chan struct{}, 10),
|
||||||
|
handlerStarted: make(chan struct{}, 10),
|
||||||
}
|
}
|
||||||
|
|
||||||
proto.RegisterFlowServiceServer(s.grpcSrv, s)
|
proto.RegisterFlowServiceServer(s.grpcSrv, s)
|
||||||
@@ -51,11 +124,23 @@ func newTestServer(t *testing.T) *testServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *testServer) Events(stream proto.FlowService_EventsServer) error {
|
func (s *testServer) Events(stream proto.FlowService_EventsServer) error {
|
||||||
|
defer func() {
|
||||||
|
select {
|
||||||
|
case s.handlerDone <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
err := stream.Send(&proto.FlowEventAck{IsInitiator: true})
|
err := stream.Send(&proto.FlowEventAck{IsInitiator: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case s.handlerStarted <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(stream.Context())
|
ctx, cancel := context.WithCancel(stream.Context())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -91,6 +176,8 @@ func (s *testServer) Events(stream proto.FlowService_EventsServer) error {
|
|||||||
if err := stream.Send(ack); err != nil {
|
if err := stream.Send(ack); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case <-s.closeStream:
|
||||||
|
return status.Errorf(codes.Internal, "server closing stream")
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
@@ -110,16 +197,13 @@ func TestReceive(t *testing.T) {
|
|||||||
assert.NoError(t, err, "failed to close flow")
|
assert.NoError(t, err, "failed to close flow")
|
||||||
})
|
})
|
||||||
|
|
||||||
receivedAcks := make(map[string]bool)
|
var ackCount atomic.Int32
|
||||||
receiveDone := make(chan struct{})
|
receiveDone := make(chan struct{})
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := client.Receive(ctx, 1*time.Second, func(msg *proto.FlowEventAck) error {
|
err := client.Receive(ctx, 1*time.Second, func(msg *proto.FlowEventAck) error {
|
||||||
if !msg.IsInitiator && len(msg.EventId) > 0 {
|
if !msg.IsInitiator && len(msg.EventId) > 0 {
|
||||||
id := string(msg.EventId)
|
if ackCount.Add(1) >= 3 {
|
||||||
receivedAcks[id] = true
|
|
||||||
|
|
||||||
if len(receivedAcks) >= 3 {
|
|
||||||
close(receiveDone)
|
close(receiveDone)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +214,11 @@ func TestReceive(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
select {
|
||||||
|
case <-server.handlerStarted:
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatal("timeout waiting for stream to be established")
|
||||||
|
}
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
eventID := uuid.New().String()
|
eventID := uuid.New().String()
|
||||||
@@ -153,7 +241,7 @@ func TestReceive(t *testing.T) {
|
|||||||
t.Fatal("timeout waiting for acks to be processed")
|
t.Fatal("timeout waiting for acks to be processed")
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, 3, len(receivedAcks))
|
assert.Equal(t, int32(3), ackCount.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReceive_ContextCancellation(t *testing.T) {
|
func TestReceive_ContextCancellation(t *testing.T) {
|
||||||
@@ -254,3 +342,195 @@ func TestSend(t *testing.T) {
|
|||||||
t.Fatal("timeout waiting for ack to be received by flow")
|
t.Fatal("timeout waiting for ack to be received by flow")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewClient_PermanentClose(t *testing.T) {
|
||||||
|
server := newTestServer(t)
|
||||||
|
|
||||||
|
client, err := flow.NewClient("http://"+server.addr, "test-payload", "test-signature", 1*time.Second)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = client.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- client.Receive(ctx, 1*time.Second, func(msg *proto.FlowEventAck) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
require.ErrorIs(t, err, flow.ErrClientClosed)
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("Receive did not return after Close — stuck in retry loop")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewClient_CloseVerify(t *testing.T) {
|
||||||
|
server := newTestServer(t)
|
||||||
|
|
||||||
|
client, err := flow.NewClient("http://"+server.addr, "test-payload", "test-signature", 1*time.Second)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
done := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
done <- client.Receive(ctx, 1*time.Second, func(msg *proto.FlowEventAck) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
|
closeDone := make(chan struct{}, 1)
|
||||||
|
go func() {
|
||||||
|
_ = client.Close()
|
||||||
|
closeDone <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
require.Error(t, err)
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("Receive did not return after Close — stuck in retry loop")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-closeDone:
|
||||||
|
return
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("Close did not return — blocked in retry loop")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClose_WhileReceiving(t *testing.T) {
|
||||||
|
server := newTestServer(t)
|
||||||
|
client, err := flow.NewClient("http://"+server.addr, "test-payload", "test-signature", 1*time.Second)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ctx := context.Background() // no timeout — intentional
|
||||||
|
receiveDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
_ = client.Receive(ctx, 1*time.Second, func(msg *proto.FlowEventAck) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
close(receiveDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for the server-side handler to confirm the stream is established.
|
||||||
|
select {
|
||||||
|
case <-server.handlerStarted:
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatal("timeout waiting for stream to be established")
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
_ = client.Close()
|
||||||
|
close(closeDone)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-closeDone:
|
||||||
|
// Close returned — good
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("Close blocked forever — Receive stuck in retry loop")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-receiveDone:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("Receive did not exit after Close")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReceive_ProtocolErrorStreamReconnect(t *testing.T) {
|
||||||
|
server := newTestServer(t)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
client, err := flow.NewClient("http://"+server.addr, "test-payload", "test-signature", 1*time.Second)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
err := client.Close()
|
||||||
|
assert.NoError(t, err, "failed to close flow")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Track acks received before and after server-side stream close
|
||||||
|
var ackCount atomic.Int32
|
||||||
|
receivedFirst := make(chan struct{})
|
||||||
|
receivedAfterReconnect := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := client.Receive(ctx, 1*time.Second, func(msg *proto.FlowEventAck) error {
|
||||||
|
if msg.IsInitiator || len(msg.EventId) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
n := ackCount.Add(1)
|
||||||
|
if n == 1 {
|
||||||
|
close(receivedFirst)
|
||||||
|
}
|
||||||
|
if n == 2 {
|
||||||
|
close(receivedAfterReconnect)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
t.Logf("receive error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Wait for stream to be established, then send first ack
|
||||||
|
select {
|
||||||
|
case <-server.handlerStarted:
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatal("timeout waiting for stream to be established")
|
||||||
|
}
|
||||||
|
server.acks <- &proto.FlowEventAck{EventId: []byte("before-close")}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-receivedFirst:
|
||||||
|
case <-time.After(3 * time.Second):
|
||||||
|
t.Fatal("timeout waiting for first ack")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot connection count before injecting the fault.
|
||||||
|
connsBefore := server.listener.connCount()
|
||||||
|
|
||||||
|
// Send a raw HTTP/2 RST_STREAM frame with PROTOCOL_ERROR on the TCP connection.
|
||||||
|
// gRPC multiplexes streams on stream IDs 1, 3, 5, ... (odd, client-initiated).
|
||||||
|
// Stream ID 1 is the client's first stream (our Events bidi stream).
|
||||||
|
// This produces the exact error the client sees in production:
|
||||||
|
// "stream terminated by RST_STREAM with error code: PROTOCOL_ERROR"
|
||||||
|
server.listener.sendRSTStream(1)
|
||||||
|
|
||||||
|
// Wait for the old Events() handler to fully exit so it can no longer
|
||||||
|
// drain s.acks and drop our injected ack on a broken stream.
|
||||||
|
select {
|
||||||
|
case <-server.handlerDone:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("old Events() handler did not exit after RST_STREAM")
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Eventually(t, func() bool {
|
||||||
|
return server.listener.connCount() > connsBefore
|
||||||
|
}, 5*time.Second, 50*time.Millisecond, "client did not open a new TCP connection after RST_STREAM")
|
||||||
|
|
||||||
|
server.acks <- &proto.FlowEventAck{EventId: []byte("after-close")}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-receivedAfterReconnect:
|
||||||
|
// Client successfully reconnected and received ack after server-side stream close
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("timeout waiting for ack after server-side stream close — client did not reconnect")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.GreaterOrEqual(t, int(ackCount.Load()), 2, "should have received acks before and after stream close")
|
||||||
|
assert.GreaterOrEqual(t, server.listener.connCount(), 2, "client should have created at least 2 TCP connections (original + reconnect)")
|
||||||
|
}
|
||||||
|
|||||||
103
go.mod
103
go.mod
@@ -13,28 +13,28 @@ require (
|
|||||||
github.com/onsi/ginkgo v1.16.5
|
github.com/onsi/ginkgo v1.16.5
|
||||||
github.com/onsi/gomega v1.27.6
|
github.com/onsi/gomega v1.27.6
|
||||||
github.com/rs/cors v1.8.0
|
github.com/rs/cors v1.8.0
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.4
|
||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.1
|
||||||
github.com/spf13/pflag v1.0.9
|
github.com/spf13/pflag v1.0.9
|
||||||
github.com/vishvananda/netlink v1.3.1
|
github.com/vishvananda/netlink v1.3.1
|
||||||
golang.org/x/crypto v0.48.0
|
golang.org/x/crypto v0.48.0
|
||||||
golang.org/x/sys v0.41.0
|
golang.org/x/sys v0.42.0
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1
|
golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||||
google.golang.org/grpc v1.79.3
|
google.golang.org/grpc v1.79.3
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
fyne.io/fyne/v2 v2.7.0
|
fyne.io/fyne/v2 v2.7.0
|
||||||
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9
|
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9
|
||||||
github.com/awnumar/memguard v0.23.0
|
github.com/awnumar/memguard v0.23.0
|
||||||
github.com/aws/aws-sdk-go-v2 v1.36.3
|
github.com/aws/aws-sdk-go-v2 v1.38.3
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.29.14
|
github.com/aws/aws-sdk-go-v2/config v1.31.6
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.10
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3
|
||||||
github.com/c-robinson/iplib v1.0.3
|
github.com/c-robinson/iplib v1.0.3
|
||||||
github.com/caddyserver/certmagic v0.21.3
|
github.com/caddyserver/certmagic v0.21.3
|
||||||
github.com/cilium/ebpf v0.15.0
|
github.com/cilium/ebpf v0.15.0
|
||||||
@@ -42,6 +42,8 @@ require (
|
|||||||
github.com/coreos/go-iptables v0.7.0
|
github.com/coreos/go-iptables v0.7.0
|
||||||
github.com/coreos/go-oidc/v3 v3.14.1
|
github.com/coreos/go-oidc/v3 v3.14.1
|
||||||
github.com/creack/pty v1.1.24
|
github.com/creack/pty v1.1.24
|
||||||
|
github.com/crowdsecurity/crowdsec v1.7.7
|
||||||
|
github.com/crowdsecurity/go-cs-bouncer v0.0.21
|
||||||
github.com/dexidp/dex v0.0.0-00010101000000-000000000000
|
github.com/dexidp/dex v0.0.0-00010101000000-000000000000
|
||||||
github.com/dexidp/dex/api/v2 v2.4.0
|
github.com/dexidp/dex/api/v2 v2.4.0
|
||||||
github.com/eko/gocache/lib/v4 v4.2.0
|
github.com/eko/gocache/lib/v4 v4.2.0
|
||||||
@@ -60,9 +62,10 @@ require (
|
|||||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357
|
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.2-0.20240212192251-757544f21357
|
||||||
github.com/hashicorp/go-multierror v1.1.1
|
github.com/hashicorp/go-multierror v1.1.1
|
||||||
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2
|
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2
|
||||||
github.com/hashicorp/go-version v1.6.0
|
github.com/hashicorp/go-version v1.7.0
|
||||||
github.com/jackc/pgx/v5 v5.5.5
|
github.com/jackc/pgx/v5 v5.5.5
|
||||||
github.com/libdns/route53 v1.5.0
|
github.com/libdns/route53 v1.5.0
|
||||||
|
github.com/libp2p/go-nat v0.2.0
|
||||||
github.com/libp2p/go-netroute v0.2.1
|
github.com/libp2p/go-netroute v0.2.1
|
||||||
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81
|
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81
|
||||||
github.com/mdlayher/socket v0.5.1
|
github.com/mdlayher/socket v0.5.1
|
||||||
@@ -103,14 +106,14 @@ require (
|
|||||||
github.com/yusufpapurcu/wmi v1.2.4
|
github.com/yusufpapurcu/wmi v1.2.4
|
||||||
github.com/zcalusic/sysinfo v1.1.3
|
github.com/zcalusic/sysinfo v1.1.3
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0
|
||||||
go.opentelemetry.io/otel v1.42.0
|
go.opentelemetry.io/otel v1.43.0
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.64.0
|
go.opentelemetry.io/otel/exporters/prometheus v0.64.0
|
||||||
go.opentelemetry.io/otel/metric v1.42.0
|
go.opentelemetry.io/otel/metric v1.43.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.42.0
|
go.opentelemetry.io/otel/sdk/metric v1.43.0
|
||||||
go.uber.org/mock v0.5.2
|
go.uber.org/mock v0.5.2
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
goauthentik.io/api/v3 v3.2023051.3
|
goauthentik.io/api/v3 v3.2023051.3
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||||
golang.org/x/mobile v0.0.0-20251113184115-a159579294ab
|
golang.org/x/mobile v0.0.0-20251113184115-a159579294ab
|
||||||
golang.org/x/mod v0.32.0
|
golang.org/x/mod v0.32.0
|
||||||
golang.org/x/net v0.51.0
|
golang.org/x/net v0.51.0
|
||||||
@@ -132,7 +135,7 @@ require (
|
|||||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||||
dario.cat/mergo v1.0.1 // indirect
|
dario.cat/mergo v1.0.1 // indirect
|
||||||
filippo.io/edwards25519 v1.1.0 // indirect
|
filippo.io/edwards25519 v1.1.1 // indirect
|
||||||
github.com/AppsFlyer/go-sundheit v0.6.0 // indirect
|
github.com/AppsFlyer/go-sundheit v0.6.0 // indirect
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
@@ -143,36 +146,39 @@ require (
|
|||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||||
github.com/awnumar/memcall v0.4.0 // indirect
|
github.com/awnumar/memcall v0.4.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
|
||||||
github.com/aws/smithy-go v1.22.2 // indirect
|
github.com/aws/smithy-go v1.23.0 // indirect
|
||||||
github.com/beevik/etree v1.6.0 // indirect
|
github.com/beevik/etree v1.6.0 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/caddyserver/zerossl v0.1.3 // indirect
|
github.com/caddyserver/zerossl v0.1.3 // indirect
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
github.com/containerd/log v0.1.0 // indirect
|
||||||
github.com/containerd/platforms v0.2.1 // indirect
|
github.com/containerd/platforms v0.2.1 // indirect
|
||||||
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
github.com/cpuguy83/dockercfg v0.3.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/crowdsecurity/go-cs-lib v0.0.25 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/docker v28.0.1+incompatible // indirect
|
github.com/docker/docker v28.0.1+incompatible // indirect
|
||||||
github.com/docker/go-connections v0.5.0 // indirect
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/ebitengine/purego v0.8.2 // indirect
|
github.com/ebitengine/purego v0.8.4 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/fredbi/uri v1.1.1 // indirect
|
github.com/fredbi/uri v1.1.1 // indirect
|
||||||
github.com/fyne-io/gl-js v0.2.0 // indirect
|
github.com/fyne-io/gl-js v0.2.0 // indirect
|
||||||
@@ -186,11 +192,23 @@ require (
|
|||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/go-openapi/analysis v0.23.0 // indirect
|
||||||
|
github.com/go-openapi/errors v0.22.2 // indirect
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1 // indirect
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/loads v0.22.0 // indirect
|
||||||
|
github.com/go-openapi/spec v0.21.0 // indirect
|
||||||
|
github.com/go-openapi/strfmt v0.23.0 // indirect
|
||||||
|
github.com/go-openapi/swag v0.23.1 // indirect
|
||||||
|
github.com/go-openapi/validate v0.24.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||||
github.com/go-text/render v0.2.0 // indirect
|
github.com/go-text/render v0.2.0 // indirect
|
||||||
github.com/go-text/typesetting v0.2.1 // indirect
|
github.com/go-text/typesetting v0.2.1 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/google/btree v1.1.2 // indirect
|
||||||
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
github.com/google/s2a-go v0.1.9 // indirect
|
github.com/google/s2a-go v0.1.9 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
|
||||||
@@ -200,24 +218,29 @@ require (
|
|||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
|
github.com/huin/goupnp v1.2.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
|
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||||
github.com/kelseyhightower/envconfig v1.4.0 // indirect
|
github.com/kelseyhightower/envconfig v1.4.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.0 // indirect
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
|
github.com/koron/go-ssdp v0.0.4 // indirect
|
||||||
github.com/kr/fs v0.1.0 // indirect
|
github.com/kr/fs v0.1.0 // indirect
|
||||||
github.com/lib/pq v1.10.9 // indirect
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/libdns/libdns v0.2.2 // indirect
|
github.com/libdns/libdns v0.2.2 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
|
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
|
||||||
github.com/magiconair/properties v1.8.10 // indirect
|
github.com/magiconair/properties v1.8.10 // indirect
|
||||||
|
github.com/mailru/easyjson v0.9.0 // indirect
|
||||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||||
github.com/mdelapenya/tlscert v0.2.0 // indirect
|
github.com/mdelapenya/tlscert v0.2.0 // indirect
|
||||||
@@ -225,6 +248,7 @@ require (
|
|||||||
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
|
||||||
github.com/mholt/acmez/v2 v2.0.1 // indirect
|
github.com/mholt/acmez/v2 v2.0.1 // indirect
|
||||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||||
@@ -237,7 +261,8 @@ require (
|
|||||||
github.com/netbirdio/management-integrations v0.0.0-20260327140942-37c286e5ead4 // indirect
|
github.com/netbirdio/management-integrations v0.0.0-20260327140942-37c286e5ead4 // indirect
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
|
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
|
||||||
github.com/nxadm/tail v1.4.8 // indirect
|
github.com/nxadm/tail v1.4.11 // indirect
|
||||||
|
github.com/oklog/ulid v1.3.1 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
@@ -247,32 +272,33 @@ require (
|
|||||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||||
github.com/pion/turn/v4 v4.1.1 // indirect
|
github.com/pion/turn/v4 v4.1.1 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.5 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
github.com/prometheus/otlptranslator v1.0.0 // indirect
|
||||||
github.com/prometheus/procfs v0.19.2 // indirect
|
github.com/prometheus/procfs v0.19.2 // indirect
|
||||||
github.com/russellhaering/goxmldsig v1.5.0 // indirect
|
github.com/russellhaering/goxmldsig v1.6.0 // indirect
|
||||||
github.com/rymdport/portal v0.4.2 // indirect
|
github.com/rymdport/portal v0.4.2 // indirect
|
||||||
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
|
github.com/shirou/gopsutil/v4 v4.25.8 // indirect
|
||||||
github.com/shoenig/go-m1cpu v0.2.1 // indirect
|
github.com/shoenig/go-m1cpu v0.2.1 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/spf13/cast v1.7.0 // indirect
|
github.com/spf13/cast v1.7.0 // indirect
|
||||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
github.com/wlynxg/anet v0.0.5 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
github.com/yuin/goldmark v1.7.8 // indirect
|
github.com/yuin/goldmark v1.7.8 // indirect
|
||||||
github.com/zeebo/blake3 v0.2.3 // indirect
|
github.com/zeebo/blake3 v0.2.3 // indirect
|
||||||
|
go.mongodb.org/mongo-driver v1.17.9 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
golang.org/x/image v0.33.0 // indirect
|
golang.org/x/image v0.33.0 // indirect
|
||||||
@@ -282,6 +308,7 @@ require (
|
|||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502
|
replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502
|
||||||
|
|||||||
209
go.sum
209
go.sum
@@ -9,8 +9,8 @@ cunicu.li/go-rosenpass v0.4.0 h1:LtPtBgFWY/9emfgC4glKLEqS0MJTylzV6+ChRhiZERw=
|
|||||||
cunicu.li/go-rosenpass v0.4.0/go.mod h1:MPbjH9nxV4l3vEagKVdFNwHOketqgS5/To1VYJplf/M=
|
cunicu.li/go-rosenpass v0.4.0/go.mod h1:MPbjH9nxV4l3vEagKVdFNwHOketqgS5/To1VYJplf/M=
|
||||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
fyne.io/fyne/v2 v2.7.0 h1:GvZSpE3X0liU/fqstInVvRsaboIVpIWQ4/sfjDGIGGQ=
|
fyne.io/fyne/v2 v2.7.0 h1:GvZSpE3X0liU/fqstInVvRsaboIVpIWQ4/sfjDGIGGQ=
|
||||||
fyne.io/fyne/v2 v2.7.0/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE=
|
fyne.io/fyne/v2 v2.7.0/go.mod h1:xClVlrhxl7D+LT+BWYmcrW4Nf+dJTvkhnPgji7spAwE=
|
||||||
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9 h1:829+77I4TaMrcg9B3wf+gHhdSgoCVEgH2czlPXPbfj4=
|
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9 h1:829+77I4TaMrcg9B3wf+gHhdSgoCVEgH2czlPXPbfj4=
|
||||||
@@ -40,48 +40,50 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
|
|||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||||
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||||
github.com/awnumar/memcall v0.4.0 h1:B7hgZYdfH6Ot1Goaz8jGne/7i8xD4taZie/PNSFZ29g=
|
github.com/awnumar/memcall v0.4.0 h1:B7hgZYdfH6Ot1Goaz8jGne/7i8xD4taZie/PNSFZ29g=
|
||||||
github.com/awnumar/memcall v0.4.0/go.mod h1:8xOx1YbfyuCg3Fy6TO8DK0kZUua3V42/goA5Ru47E8w=
|
github.com/awnumar/memcall v0.4.0/go.mod h1:8xOx1YbfyuCg3Fy6TO8DK0kZUua3V42/goA5Ru47E8w=
|
||||||
github.com/awnumar/memguard v0.23.0 h1:sJ3a1/SWlcuKIQ7MV+R9p0Pvo9CWsMbGZvcZQtmc68A=
|
github.com/awnumar/memguard v0.23.0 h1:sJ3a1/SWlcuKIQ7MV+R9p0Pvo9CWsMbGZvcZQtmc68A=
|
||||||
github.com/awnumar/memguard v0.23.0/go.mod h1:olVofBrsPdITtJ2HgxQKrEYEMyIBAIciVG4wNnZhW9M=
|
github.com/awnumar/memguard v0.23.0/go.mod h1:olVofBrsPdITtJ2HgxQKrEYEMyIBAIciVG4wNnZhW9M=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
|
github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=
|
||||||
github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
|
github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
|
||||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
|
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM=
|
github.com/aws/aws-sdk-go-v2/config v1.31.6 h1:a1t8fXY4GT4xjyJExz4knbuoxSCacB5hT/WgtfPyLjo=
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g=
|
github.com/aws/aws-sdk-go-v2/config v1.31.6/go.mod h1:5ByscNi7R+ztvOGzeUaIu49vkMk2soq5NaH5PYe33MQ=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM=
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg=
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ=
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 h1:wbjnrrMnKew78/juW7I2BtKQwa1qlf6EjQgS69uYY14=
|
||||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6/go.mod h1:AtiqqNrDioJXuUgz3+3T0mBWN7Hro2n9wll2zRUc0ww=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
|
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
|
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 h1:R0tNFJqfjHL3900cqhXuwQ+1K4G0xc9Yf8EDbFXCKEw=
|
||||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
|
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6/go.mod h1:y/7sDdu+aJvPtGXr4xYosdpq9a6T9Z0jkXfugmti0rI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
|
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU=
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 h1:hncKj/4gR+TPauZgTAsxOxNcvBayhUlYZ6LO/BYiQ30=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
|
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6/go.mod h1:OiIh45tp6HdJDDJGnja0mw8ihQGz3VGrUflLqSL0SmM=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 h1:LHS1YAIJXJ4K9zS+1d/xa9JAA9sL2QyXIQCQFQW/X08=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6/go.mod h1:c9PCiTEuh0wQID5/KqA32J+HAgZxN9tOGXKCiYJjTZI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 h1:nEXUSAwyUfLTgnc9cxlDWy637qsq4UWwp3sNAfl0Z3Y=
|
||||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
|
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6/go.mod h1:HGzIULx4Ge3Do2V0FaiYKcyKzOqwrhUZgCI77NisswQ=
|
||||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3 h1:MmLCRqP4U4Cw9gJ4bNrCG0mWqEtBlmAVleyelcHARMU=
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3 h1:MmLCRqP4U4Cw9gJ4bNrCG0mWqEtBlmAVleyelcHARMU=
|
||||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3/go.mod h1:AMPjK2YnRh0YgOID3PqhJA1BRNfXDfGOnSsKHtAe8yA=
|
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3/go.mod h1:AMPjK2YnRh0YgOID3PqhJA1BRNfXDfGOnSsKHtAe8yA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2 h1:tWUG+4wZqdMl/znThEk9tcCy8tTMxq8dW0JTgamohrY=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3 h1:ETkfWcXP2KNPLecaDa++5bsQhCRa5M5sLUJa5DWYIIg=
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc=
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3/go.mod h1:+/3ZTqoYb3Ur7DObD00tarKMLMuKg8iqz5CHEanqTnw=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 h1:8OLZnVJPvjnrxEwHFg9hVUof/P4sibH+Ea4KKuqAGSg=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
|
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1/go.mod h1:27M3BpVi0C02UiQh1w9nsBEit6pLhlaH3NHna6WUbDE=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 h1:gKWSTnqudpo8dAxqBqZnDoDWCiEh/40FziUjr/mo6uA=
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2/go.mod h1:x7+rkNmRoEN1U13A6JE2fXne9EWyJy54o3n6d4mGaXQ=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl7fiGcfJTIo8BszI=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c=
|
||||||
github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
|
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||||
github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||||
github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE=
|
github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE=
|
||||||
github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc=
|
github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
@@ -99,6 +101,8 @@ github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+Y
|
|||||||
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
|
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
|
||||||
@@ -118,11 +122,18 @@ github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHf
|
|||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
|
github.com/crowdsecurity/crowdsec v1.7.7 h1:sduZN763iXsrZodocWDrsR//7nLeffGu+RVkkIsbQkE=
|
||||||
|
github.com/crowdsecurity/crowdsec v1.7.7/go.mod h1:L1HLGPDnBYCcY+yfSFnuBbQ1G9DHEJN9c+Kevv9F+4Q=
|
||||||
|
github.com/crowdsecurity/go-cs-bouncer v0.0.21 h1:arPz0VtdVSaz+auOSfHythzkZVLyy18CzYvYab8UJDU=
|
||||||
|
github.com/crowdsecurity/go-cs-bouncer v0.0.21/go.mod h1:4JiH0XXA4KKnnWThItUpe5+heJHWzsLOSA2IWJqUDBA=
|
||||||
|
github.com/crowdsecurity/go-cs-lib v0.0.25 h1:Ov6VPW9yV+OPsbAIQk1iTkEWhwkpaG0v3lrBzeqjzj4=
|
||||||
|
github.com/crowdsecurity/go-cs-lib v0.0.25/go.mod h1:X0GMJY2CxdA1S09SpuqIKaWQsvRGxXmecUp9cP599dE=
|
||||||
github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6 h1:/DS5cDX3FJdl+XaN2D7XAwFpuanTxnp52DBLZAaJKx0=
|
github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6 h1:/DS5cDX3FJdl+XaN2D7XAwFpuanTxnp52DBLZAaJKx0=
|
||||||
github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
|
github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dexidp/dex/api/v2 v2.4.0 h1:gNba7n6BKVp8X4Jp24cxYn5rIIGhM6kDOXcZoL6tr9A=
|
github.com/dexidp/dex/api/v2 v2.4.0 h1:gNba7n6BKVp8X4Jp24cxYn5rIIGhM6kDOXcZoL6tr9A=
|
||||||
github.com/dexidp/dex/api/v2 v2.4.0/go.mod h1:/p550ADvFFh7K95VmhUD+jgm15VdaNnab9td8DHOpyI=
|
github.com/dexidp/dex/api/v2 v2.4.0/go.mod h1:/p550ADvFFh7K95VmhUD+jgm15VdaNnab9td8DHOpyI=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
@@ -131,12 +142,12 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
|||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
|
github.com/docker/docker v28.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0=
|
||||||
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/eko/gocache/lib/v4 v4.2.0 h1:MNykyi5Xw+5Wu3+PUrvtOCaKSZM1nUSVftbzmeC7Yuw=
|
github.com/eko/gocache/lib/v4 v4.2.0 h1:MNykyi5Xw+5Wu3+PUrvtOCaKSZM1nUSVftbzmeC7Yuw=
|
||||||
github.com/eko/gocache/lib/v4 v4.2.0/go.mod h1:7ViVmbU+CzDHzRpmB4SXKyyzyuJ8A3UW3/cszpcqB4M=
|
github.com/eko/gocache/lib/v4 v4.2.0/go.mod h1:7ViVmbU+CzDHzRpmB4SXKyyzyuJ8A3UW3/cszpcqB4M=
|
||||||
github.com/eko/gocache/store/go_cache/v4 v4.2.2 h1:tAI9nl6TLoJyKG1ujF0CS0n/IgTEMl+NivxtR5R3/hw=
|
github.com/eko/gocache/store/go_cache/v4 v4.2.2 h1:tAI9nl6TLoJyKG1ujF0CS0n/IgTEMl+NivxtR5R3/hw=
|
||||||
@@ -155,6 +166,7 @@ github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
|
|||||||
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
|
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
|
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
|
||||||
@@ -187,6 +199,24 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
|
|||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
|
||||||
|
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
|
||||||
|
github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg=
|
||||||
|
github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
|
||||||
|
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
|
||||||
|
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
|
||||||
|
github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
|
||||||
|
github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=
|
||||||
|
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
|
||||||
|
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
|
||||||
|
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
|
||||||
|
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
|
||||||
|
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
|
||||||
|
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
|
||||||
|
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
|
||||||
|
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
|
||||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||||
@@ -203,10 +233,14 @@ github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg
|
|||||||
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
|
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
|
||||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
|
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
|
||||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||||
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
@@ -230,6 +264,7 @@ github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl76
|
|||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
@@ -237,6 +272,8 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN
|
|||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
|
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||||
@@ -276,11 +313,13 @@ github.com/hashicorp/go-secure-stdlib/base62 v0.1.2/go.mod h1:EdWO6czbmthiwZ3/PU
|
|||||||
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
|
||||||
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||||
|
github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY=
|
||||||
|
github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||||
@@ -291,6 +330,8 @@ github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
|
|||||||
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
|
||||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||||
|
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||||
|
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||||
@@ -315,6 +356,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw
|
|||||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
||||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||||
@@ -326,8 +369,10 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
|||||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
|
||||||
|
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
|
||||||
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
@@ -346,6 +391,8 @@ github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
|
|||||||
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||||
github.com/libdns/route53 v1.5.0 h1:2SKdpPFl/qgWsXQvsLNJJAoX7rSxlk7zgoL4jnWdXVA=
|
github.com/libdns/route53 v1.5.0 h1:2SKdpPFl/qgWsXQvsLNJJAoX7rSxlk7zgoL4jnWdXVA=
|
||||||
github.com/libdns/route53 v1.5.0/go.mod h1:joT4hKmaTNKHEwb7GmZ65eoDz1whTu7KKYPS8ZqIh6Q=
|
github.com/libdns/route53 v1.5.0/go.mod h1:joT4hKmaTNKHEwb7GmZ65eoDz1whTu7KKYPS8ZqIh6Q=
|
||||||
|
github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk=
|
||||||
|
github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk=
|
||||||
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81 h1:J56rFEfUTFT9j9CiRXhi1r8lUJ4W5idG3CiaBZGojNU=
|
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81 h1:J56rFEfUTFT9j9CiRXhi1r8lUJ4W5idG3CiaBZGojNU=
|
||||||
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81/go.mod h1:RD8ML/YdXctQ7qbcizZkw5mZ6l8Ogrl1dodBzVJduwI=
|
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81/go.mod h1:RD8ML/YdXctQ7qbcizZkw5mZ6l8Ogrl1dodBzVJduwI=
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
@@ -353,6 +400,8 @@ github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tA
|
|||||||
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||||
|
github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
|
||||||
|
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
||||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
|
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
|
||||||
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
|
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
|
||||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
@@ -376,6 +425,8 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1
|
|||||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
@@ -419,10 +470,13 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S
|
|||||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
||||||
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
|
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
|
||||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||||
|
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
|
||||||
|
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
|
||||||
github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI=
|
github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI=
|
||||||
github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||||
|
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||||
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/okta/okta-sdk-golang/v2 v2.18.0 h1:cfDasMb7CShbZvOrF6n+DnLevWwiHgedWMGJ8M8xKDc=
|
github.com/okta/okta-sdk-golang/v2 v2.18.0 h1:cfDasMb7CShbZvOrF6n+DnLevWwiHgedWMGJ8M8xKDc=
|
||||||
github.com/okta/okta-sdk-golang/v2 v2.18.0/go.mod h1:dz30v3ctAiMb7jpsCngGfQUAEGm1/NsWT92uTbNDQIs=
|
github.com/okta/okta-sdk-golang/v2 v2.18.0/go.mod h1:dz30v3ctAiMb7jpsCngGfQUAEGm1/NsWT92uTbNDQIs=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
@@ -443,8 +497,8 @@ github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq5
|
|||||||
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
|
github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 h1:E7Kmf11E4K7B5hDti2K2NqPb1nlYlGYsu02S1JNd/Bs=
|
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 h1:E7Kmf11E4K7B5hDti2K2NqPb1nlYlGYsu02S1JNd/Bs=
|
||||||
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||||
@@ -482,8 +536,9 @@ github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
|||||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||||
github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw=
|
github.com/pkg/sftp v1.13.9 h1:4NGkvGudBL7GteO3m6qnaQ4pC0Kvf0onSVc9gR3EWBw=
|
||||||
github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA=
|
github.com/pkg/sftp v1.13.9/go.mod h1:OBN7bVXdstkFFN/gdnHPUb5TE8eb8G1Rp9wCItqjkkA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
@@ -507,15 +562,15 @@ github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so=
|
|||||||
github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM=
|
github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM=
|
||||||
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
|
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
|
||||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/russellhaering/goxmldsig v1.5.0 h1:AU2UkkYIUOTyZRbe08XMThaOCelArgvNfYapcmSjBNw=
|
github.com/russellhaering/goxmldsig v1.6.0 h1:8fdWXEPh2k/NZNQBPFNoVfS3JmzS4ZprY/sAOpKQLks=
|
||||||
github.com/russellhaering/goxmldsig v1.5.0/go.mod h1:x98CjQNFJcWfMxeOrMnMKg70lvDP6tE0nTaeUnjXDmk=
|
github.com/russellhaering/goxmldsig v1.6.0/go.mod h1:TrnaquDcYxWXfJrOjeMBTX4mLBeYAqaHEyUeWPxZlBM=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
|
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
|
||||||
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
|
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
|
||||||
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
|
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
|
||||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||||
github.com/shoenig/go-m1cpu v0.2.1 h1:yqRB4fvOge2+FyRXFkXqsyMoqPazv14Yyy+iyccT2E4=
|
github.com/shoenig/go-m1cpu v0.2.1 h1:yqRB4fvOge2+FyRXFkXqsyMoqPazv14Yyy+iyccT2E4=
|
||||||
github.com/shoenig/go-m1cpu v0.2.1/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=
|
github.com/shoenig/go-m1cpu v0.2.1/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=
|
||||||
@@ -524,8 +579,8 @@ github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
|
|||||||
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
|
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
|
||||||
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
|
||||||
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
|
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
|
||||||
@@ -574,11 +629,11 @@ github.com/ti-mo/conntrack v0.5.1/go.mod h1:T6NCbkMdVU4qEIgwL0njA6lw/iCAbzchlnwm
|
|||||||
github.com/ti-mo/netfilter v0.5.2 h1:CTjOwFuNNeZ9QPdRXt1MZFLFUf84cKtiQutNauHWd40=
|
github.com/ti-mo/netfilter v0.5.2 h1:CTjOwFuNNeZ9QPdRXt1MZFLFUf84cKtiQutNauHWd40=
|
||||||
github.com/ti-mo/netfilter v0.5.2/go.mod h1:Btx3AtFiOVdHReTDmP9AE+hlkOcvIy403u7BXXbWZKo=
|
github.com/ti-mo/netfilter v0.5.2/go.mod h1:Btx3AtFiOVdHReTDmP9AE+hlkOcvIy403u7BXXbWZKo=
|
||||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||||
@@ -607,28 +662,30 @@ github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
|
|||||||
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
|
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
|
||||||
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||||
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
||||||
|
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs=
|
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs=
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro=
|
go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro=
|
||||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
|
||||||
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
@@ -656,8 +713,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
|
|||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
|
||||||
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
@@ -729,8 +786,8 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -744,8 +801,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|||||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -832,8 +889,8 @@ gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8
|
|||||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
|||||||
@@ -302,7 +302,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "rate(management_account_peer_meta_update_counter_ratio_total{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])",
|
"expr": "rate(management_account_peer_meta_update_counter_total{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])",
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "{{cluster}}/{{environment}}/{{job}}",
|
"legendFormat": "{{cluster}}/{{environment}}/{{job}}",
|
||||||
"range": true,
|
"range": true,
|
||||||
@@ -410,7 +410,7 @@
|
|||||||
},
|
},
|
||||||
"disableTextWrap": false,
|
"disableTextWrap": false,
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.5,sum(increase(management_account_get_peer_network_map_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
"expr": "histogram_quantile(0.5,sum(increase(management_account_get_peer_network_map_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
||||||
"format": "heatmap",
|
"format": "heatmap",
|
||||||
"fullMetaSearch": false,
|
"fullMetaSearch": false,
|
||||||
"includeNullMetadata": true,
|
"includeNullMetadata": true,
|
||||||
@@ -426,7 +426,7 @@
|
|||||||
},
|
},
|
||||||
"disableTextWrap": false,
|
"disableTextWrap": false,
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.9,sum(increase(management_account_get_peer_network_map_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
"expr": "histogram_quantile(0.9,sum(increase(management_account_get_peer_network_map_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
||||||
"format": "heatmap",
|
"format": "heatmap",
|
||||||
"fullMetaSearch": false,
|
"fullMetaSearch": false,
|
||||||
"hide": false,
|
"hide": false,
|
||||||
@@ -443,7 +443,7 @@
|
|||||||
},
|
},
|
||||||
"disableTextWrap": false,
|
"disableTextWrap": false,
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.99,sum(increase(management_account_get_peer_network_map_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
"expr": "histogram_quantile(0.99,sum(increase(management_account_get_peer_network_map_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
||||||
"format": "heatmap",
|
"format": "heatmap",
|
||||||
"fullMetaSearch": false,
|
"fullMetaSearch": false,
|
||||||
"hide": false,
|
"hide": false,
|
||||||
@@ -545,7 +545,7 @@
|
|||||||
},
|
},
|
||||||
"disableTextWrap": false,
|
"disableTextWrap": false,
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.5,sum(increase(management_account_update_account_peers_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
"expr": "histogram_quantile(0.5,sum(increase(management_account_update_account_peers_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
||||||
"format": "heatmap",
|
"format": "heatmap",
|
||||||
"fullMetaSearch": false,
|
"fullMetaSearch": false,
|
||||||
"includeNullMetadata": true,
|
"includeNullMetadata": true,
|
||||||
@@ -561,7 +561,7 @@
|
|||||||
},
|
},
|
||||||
"disableTextWrap": false,
|
"disableTextWrap": false,
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.9,sum(increase(management_account_update_account_peers_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
"expr": "histogram_quantile(0.9,sum(increase(management_account_update_account_peers_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
||||||
"format": "heatmap",
|
"format": "heatmap",
|
||||||
"fullMetaSearch": false,
|
"fullMetaSearch": false,
|
||||||
"hide": false,
|
"hide": false,
|
||||||
@@ -578,7 +578,7 @@
|
|||||||
},
|
},
|
||||||
"disableTextWrap": false,
|
"disableTextWrap": false,
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.99,sum(increase(management_account_update_account_peers_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
"expr": "histogram_quantile(0.99,sum(increase(management_account_update_account_peers_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
||||||
"format": "heatmap",
|
"format": "heatmap",
|
||||||
"fullMetaSearch": false,
|
"fullMetaSearch": false,
|
||||||
"hide": false,
|
"hide": false,
|
||||||
@@ -694,7 +694,7 @@
|
|||||||
},
|
},
|
||||||
"disableTextWrap": false,
|
"disableTextWrap": false,
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.5,sum(increase(management_grpc_updatechannel_queue_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
"expr": "histogram_quantile(0.5,sum(increase(management_grpc_updatechannel_queue_length_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
||||||
"format": "heatmap",
|
"format": "heatmap",
|
||||||
"fullMetaSearch": false,
|
"fullMetaSearch": false,
|
||||||
"includeNullMetadata": true,
|
"includeNullMetadata": true,
|
||||||
@@ -710,7 +710,7 @@
|
|||||||
},
|
},
|
||||||
"disableTextWrap": false,
|
"disableTextWrap": false,
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.9,sum(increase(management_grpc_updatechannel_queue_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
"expr": "histogram_quantile(0.9,sum(increase(management_grpc_updatechannel_queue_length_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
||||||
"format": "heatmap",
|
"format": "heatmap",
|
||||||
"fullMetaSearch": false,
|
"fullMetaSearch": false,
|
||||||
"hide": false,
|
"hide": false,
|
||||||
@@ -727,7 +727,7 @@
|
|||||||
},
|
},
|
||||||
"disableTextWrap": false,
|
"disableTextWrap": false,
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.99,sum(increase(management_grpc_updatechannel_queue_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
"expr": "histogram_quantile(0.99,sum(increase(management_grpc_updatechannel_queue_length_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le,cluster,environment,job))",
|
||||||
"format": "heatmap",
|
"format": "heatmap",
|
||||||
"fullMetaSearch": false,
|
"fullMetaSearch": false,
|
||||||
"hide": false,
|
"hide": false,
|
||||||
@@ -841,7 +841,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.50, sum(rate(management_store_persistence_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
"expr": "histogram_quantile(0.50, sum(rate(management_store_persistence_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p50",
|
"legendFormat": "p50",
|
||||||
"range": true,
|
"range": true,
|
||||||
@@ -853,7 +853,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.90, sum(rate(management_store_persistence_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
"expr": "histogram_quantile(0.90, sum(rate(management_store_persistence_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
||||||
"hide": false,
|
"hide": false,
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p90",
|
"legendFormat": "p90",
|
||||||
@@ -866,7 +866,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.99, sum(rate(management_store_persistence_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
"expr": "histogram_quantile(0.99, sum(rate(management_store_persistence_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
||||||
"hide": false,
|
"hide": false,
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p99",
|
"legendFormat": "p99",
|
||||||
@@ -963,7 +963,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.50, sum(rate(management_store_transaction_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
"expr": "histogram_quantile(0.50, sum(rate(management_store_transaction_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p50",
|
"legendFormat": "p50",
|
||||||
"range": true,
|
"range": true,
|
||||||
@@ -975,7 +975,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.90, sum(rate(management_store_transaction_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
"expr": "histogram_quantile(0.90, sum(rate(management_store_transaction_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
||||||
"hide": false,
|
"hide": false,
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p90",
|
"legendFormat": "p90",
|
||||||
@@ -988,7 +988,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.99, sum(rate(management_store_transaction_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
"expr": "histogram_quantile(0.99, sum(rate(management_store_transaction_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
||||||
"hide": false,
|
"hide": false,
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p99",
|
"legendFormat": "p99",
|
||||||
@@ -1085,7 +1085,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.50, sum(rate(management_store_global_lock_acquisition_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
"expr": "histogram_quantile(0.50, sum(rate(management_store_global_lock_acquisition_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p50",
|
"legendFormat": "p50",
|
||||||
"range": true,
|
"range": true,
|
||||||
@@ -1097,7 +1097,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.90, sum(rate(management_store_global_lock_acquisition_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
"expr": "histogram_quantile(0.90, sum(rate(management_store_global_lock_acquisition_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
||||||
"hide": false,
|
"hide": false,
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p90",
|
"legendFormat": "p90",
|
||||||
@@ -1110,7 +1110,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.99, sum(rate(management_store_global_lock_acquisition_duration_ms_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
"expr": "histogram_quantile(0.99, sum(rate(management_store_global_lock_acquisition_duration_ms_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (le))",
|
||||||
"hide": false,
|
"hide": false,
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p99",
|
"legendFormat": "p99",
|
||||||
@@ -1221,7 +1221,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "rate(management_idp_authenticate_request_counter_ratio_total{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])",
|
"expr": "rate(management_idp_authenticate_request_counter_total{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])",
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "{{cluster}}/{{environment}}/{{job}}",
|
"legendFormat": "{{cluster}}/{{environment}}/{{job}}",
|
||||||
"range": true,
|
"range": true,
|
||||||
@@ -1317,7 +1317,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "rate(management_idp_get_account_counter_ratio_total{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])",
|
"expr": "rate(management_idp_get_account_counter_total{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])",
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "{{cluster}}/{{environment}}/{{job}}",
|
"legendFormat": "{{cluster}}/{{environment}}/{{job}}",
|
||||||
"range": true,
|
"range": true,
|
||||||
@@ -1413,7 +1413,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "rate(management_idp_update_user_meta_counter_ratio_total{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])",
|
"expr": "rate(management_idp_update_user_meta_counter_total{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])",
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "{{cluster}}/{{environment}}/{{job}}",
|
"legendFormat": "{{cluster}}/{{environment}}/{{job}}",
|
||||||
"range": true,
|
"range": true,
|
||||||
@@ -1523,7 +1523,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "sum(rate(management_http_request_counter_ratio_total{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",method=~\"GET|OPTIONS\"}[$__rate_interval])) by (job,method)",
|
"expr": "sum(rate(management_http_request_counter_total{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",method=~\"GET|OPTIONS\"}[$__rate_interval])) by (job,method)",
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "{{method}}",
|
"legendFormat": "{{method}}",
|
||||||
"range": true,
|
"range": true,
|
||||||
@@ -1619,7 +1619,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "sum(rate(management_http_request_counter_ratio_total{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",method=~\"POST|PUT|DELETE\"}[$__rate_interval])) by (job,method)",
|
"expr": "sum(rate(management_http_request_counter_total{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",method=~\"POST|PUT|DELETE\"}[$__rate_interval])) by (job,method)",
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "{{method}}",
|
"legendFormat": "{{method}}",
|
||||||
"range": true,
|
"range": true,
|
||||||
@@ -1715,7 +1715,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.50, sum(rate(management_http_request_duration_ms_total_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",type=~\"read\"}[5m])) by (le))",
|
"expr": "histogram_quantile(0.50, sum(rate(management_http_request_duration_ms_total_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",type=~\"read\"}[5m])) by (le))",
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p50",
|
"legendFormat": "p50",
|
||||||
"range": true,
|
"range": true,
|
||||||
@@ -1727,7 +1727,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.90, sum(rate(management_http_request_duration_ms_total_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",type=~\"read\"}[5m])) by (le))",
|
"expr": "histogram_quantile(0.90, sum(rate(management_http_request_duration_ms_total_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",type=~\"read\"}[5m])) by (le))",
|
||||||
"hide": false,
|
"hide": false,
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p90",
|
"legendFormat": "p90",
|
||||||
@@ -1740,7 +1740,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.99, sum(rate(management_http_request_duration_ms_total_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",type=~\"read\"}[5m])) by (le))",
|
"expr": "histogram_quantile(0.99, sum(rate(management_http_request_duration_ms_total_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",type=~\"read\"}[5m])) by (le))",
|
||||||
"hide": false,
|
"hide": false,
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p99",
|
"legendFormat": "p99",
|
||||||
@@ -1837,7 +1837,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.50, sum(rate(management_http_request_duration_ms_total_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",type=~\"write\"}[5m])) by (le))",
|
"expr": "histogram_quantile(0.50, sum(rate(management_http_request_duration_ms_total_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",type=~\"write\"}[5m])) by (le))",
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p50",
|
"legendFormat": "p50",
|
||||||
"range": true,
|
"range": true,
|
||||||
@@ -1849,7 +1849,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.90, sum(rate(management_http_request_duration_ms_total_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",type=~\"write\"}[5m])) by (le))",
|
"expr": "histogram_quantile(0.90, sum(rate(management_http_request_duration_ms_total_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",type=~\"write\"}[5m])) by (le))",
|
||||||
"hide": false,
|
"hide": false,
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p90",
|
"legendFormat": "p90",
|
||||||
@@ -1862,7 +1862,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "histogram_quantile(0.99, sum(rate(management_http_request_duration_ms_total_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",type=~\"write\"}[5m])) by (le))",
|
"expr": "histogram_quantile(0.99, sum(rate(management_http_request_duration_ms_total_milliseconds_bucket{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\",type=~\"write\"}[5m])) by (le))",
|
||||||
"hide": false,
|
"hide": false,
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "p99",
|
"legendFormat": "p99",
|
||||||
@@ -1963,7 +1963,7 @@
|
|||||||
"uid": "${datasource}"
|
"uid": "${datasource}"
|
||||||
},
|
},
|
||||||
"editorMode": "code",
|
"editorMode": "code",
|
||||||
"expr": "sum(rate(management_http_request_counter_ratio_total{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (job,exported_endpoint,method)",
|
"expr": "sum(rate(management_http_request_counter_total{cluster=~\"$cluster\",environment=~\"$environment\",job=~\"$job\",host=~\"$host\"}[$__rate_interval])) by (job,exported_endpoint,method)",
|
||||||
"hide": false,
|
"hide": false,
|
||||||
"instant": false,
|
"instant": false,
|
||||||
"legendFormat": "{{method}}-{{exported_endpoint}}",
|
"legendFormat": "{{method}}-{{exported_endpoint}}",
|
||||||
@@ -3222,7 +3222,7 @@
|
|||||||
},
|
},
|
||||||
"disableTextWrap": false,
|
"disableTextWrap": false,
|
||||||
"editorMode": "builder",
|
"editorMode": "builder",
|
||||||
"expr": "sum by(le) (increase(management_grpc_updatechannel_queue_bucket{application=\"management\", environment=\"$environment\", host=~\"$host\"}[$__rate_interval]))",
|
"expr": "sum by(le) (increase(management_grpc_updatechannel_queue_length_bucket{application=\"management\", environment=\"$environment\", host=~\"$host\"}[$__rate_interval]))",
|
||||||
"format": "heatmap",
|
"format": "heatmap",
|
||||||
"fullMetaSearch": false,
|
"fullMetaSearch": false,
|
||||||
"includeNullMetadata": true,
|
"includeNullMetadata": true,
|
||||||
@@ -3323,7 +3323,7 @@
|
|||||||
},
|
},
|
||||||
"disableTextWrap": false,
|
"disableTextWrap": false,
|
||||||
"editorMode": "builder",
|
"editorMode": "builder",
|
||||||
"expr": "sum by(le) (increase(management_account_update_account_peers_duration_ms_bucket{application=\"management\", environment=\"$environment\", host=~\"$host\"}[$__rate_interval]))",
|
"expr": "sum by(le) (increase(management_account_update_account_peers_duration_ms_milliseconds_bucket{application=\"management\", environment=\"$environment\", host=~\"$host\"}[$__rate_interval]))",
|
||||||
"format": "heatmap",
|
"format": "heatmap",
|
||||||
"fullMetaSearch": false,
|
"fullMetaSearch": false,
|
||||||
"includeNullMetadata": true,
|
"includeNullMetadata": true,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package accesslogs
|
package accesslogs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"maps"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
@@ -37,6 +38,7 @@ type AccessLogEntry struct {
|
|||||||
BytesUpload int64 `gorm:"index"`
|
BytesUpload int64 `gorm:"index"`
|
||||||
BytesDownload int64 `gorm:"index"`
|
BytesDownload int64 `gorm:"index"`
|
||||||
Protocol AccessLogProtocol `gorm:"index"`
|
Protocol AccessLogProtocol `gorm:"index"`
|
||||||
|
Metadata map[string]string `gorm:"serializer:json"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromProto creates an AccessLogEntry from a proto.AccessLog
|
// FromProto creates an AccessLogEntry from a proto.AccessLog
|
||||||
@@ -55,6 +57,7 @@ func (a *AccessLogEntry) FromProto(serviceLog *proto.AccessLog) {
|
|||||||
a.BytesUpload = serviceLog.GetBytesUpload()
|
a.BytesUpload = serviceLog.GetBytesUpload()
|
||||||
a.BytesDownload = serviceLog.GetBytesDownload()
|
a.BytesDownload = serviceLog.GetBytesDownload()
|
||||||
a.Protocol = AccessLogProtocol(serviceLog.GetProtocol())
|
a.Protocol = AccessLogProtocol(serviceLog.GetProtocol())
|
||||||
|
a.Metadata = maps.Clone(serviceLog.GetMetadata())
|
||||||
|
|
||||||
if sourceIP := serviceLog.GetSourceIp(); sourceIP != "" {
|
if sourceIP := serviceLog.GetSourceIp(); sourceIP != "" {
|
||||||
if addr, err := netip.ParseAddr(sourceIP); err == nil {
|
if addr, err := netip.ParseAddr(sourceIP); err == nil {
|
||||||
@@ -117,6 +120,11 @@ func (a *AccessLogEntry) ToAPIResponse() *api.ProxyAccessLog {
|
|||||||
protocol = &p
|
protocol = &p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var metadata *map[string]string
|
||||||
|
if len(a.Metadata) > 0 {
|
||||||
|
metadata = &a.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
return &api.ProxyAccessLog{
|
return &api.ProxyAccessLog{
|
||||||
Id: a.ID,
|
Id: a.ID,
|
||||||
ServiceId: a.ServiceID,
|
ServiceId: a.ServiceID,
|
||||||
@@ -136,5 +144,6 @@ func (a *AccessLogEntry) ToAPIResponse() *api.ProxyAccessLog {
|
|||||||
BytesUpload: a.BytesUpload,
|
BytesUpload: a.BytesUpload,
|
||||||
BytesDownload: a.BytesDownload,
|
BytesDownload: a.BytesDownload,
|
||||||
Protocol: protocol,
|
Protocol: protocol,
|
||||||
|
Metadata: metadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,13 +92,23 @@ func (m *managerImpl) CleanupOldAccessLogs(ctx context.Context, retentionDays in
|
|||||||
|
|
||||||
// StartPeriodicCleanup starts a background goroutine that periodically cleans up old access logs
|
// StartPeriodicCleanup starts a background goroutine that periodically cleans up old access logs
|
||||||
func (m *managerImpl) StartPeriodicCleanup(ctx context.Context, retentionDays, cleanupIntervalHours int) {
|
func (m *managerImpl) StartPeriodicCleanup(ctx context.Context, retentionDays, cleanupIntervalHours int) {
|
||||||
if retentionDays <= 0 {
|
if retentionDays < 0 {
|
||||||
log.WithContext(ctx).Debug("periodic access log cleanup disabled: retention days is 0 or negative")
|
log.WithContext(ctx).Debug("periodic access log cleanup disabled: retention days is negative")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if retentionDays == 0 {
|
||||||
|
retentionDays = 7
|
||||||
|
log.WithContext(ctx).Debugf("no retention days specified for access log cleanup, defaulting to %d days", retentionDays)
|
||||||
|
} else {
|
||||||
|
log.WithContext(ctx).Debugf("access log retention period set to %d days", retentionDays)
|
||||||
|
}
|
||||||
|
|
||||||
if cleanupIntervalHours <= 0 {
|
if cleanupIntervalHours <= 0 {
|
||||||
cleanupIntervalHours = 24
|
cleanupIntervalHours = 24
|
||||||
|
log.WithContext(ctx).Debugf("no cleanup interval specified for access log cleanup, defaulting to %d hours", cleanupIntervalHours)
|
||||||
|
} else {
|
||||||
|
log.WithContext(ctx).Debugf("access log cleanup interval set to %d hours", cleanupIntervalHours)
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupCtx, cancel := context.WithCancel(ctx)
|
cleanupCtx, cancel := context.WithCancel(ctx)
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ func TestCleanupWithExactBoundary(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestStartPeriodicCleanup(t *testing.T) {
|
func TestStartPeriodicCleanup(t *testing.T) {
|
||||||
t.Run("periodic cleanup disabled with zero retention", func(t *testing.T) {
|
t.Run("periodic cleanup disabled with negative retention", func(t *testing.T) {
|
||||||
ctrl := gomock.NewController(t)
|
ctrl := gomock.NewController(t)
|
||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ func TestStartPeriodicCleanup(t *testing.T) {
|
|||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
manager.StartPeriodicCleanup(ctx, 0, 1)
|
manager.StartPeriodicCleanup(ctx, -1, 1)
|
||||||
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ type Domain struct {
|
|||||||
// RequireSubdomain is populated at query time. When true, the domain
|
// RequireSubdomain is populated at query time. When true, the domain
|
||||||
// cannot be used bare and a subdomain label must be prepended. Not persisted.
|
// cannot be used bare and a subdomain label must be prepended. Not persisted.
|
||||||
RequireSubdomain *bool `gorm:"-"`
|
RequireSubdomain *bool `gorm:"-"`
|
||||||
|
// SupportsCrowdSec is populated at query time from proxy cluster capabilities.
|
||||||
|
// Not persisted.
|
||||||
|
SupportsCrowdSec *bool `gorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EventMeta returns activity event metadata for a domain
|
// EventMeta returns activity event metadata for a domain
|
||||||
@@ -30,3 +33,8 @@ func (d *Domain) EventMeta() map[string]any {
|
|||||||
"validated": d.Validated,
|
"validated": d.Validated,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Domain) Copy() *Domain {
|
||||||
|
dCopy := *d
|
||||||
|
return &dCopy
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ func domainToApi(d *domain.Domain) api.ReverseProxyDomain {
|
|||||||
Validated: d.Validated,
|
Validated: d.Validated,
|
||||||
SupportsCustomPorts: d.SupportsCustomPorts,
|
SupportsCustomPorts: d.SupportsCustomPorts,
|
||||||
RequireSubdomain: d.RequireSubdomain,
|
RequireSubdomain: d.RequireSubdomain,
|
||||||
|
SupportsCrowdsec: d.SupportsCrowdSec,
|
||||||
}
|
}
|
||||||
if d.TargetCluster != "" {
|
if d.TargetCluster != "" {
|
||||||
resp.TargetCluster = &d.TargetCluster
|
resp.TargetCluster = &d.TargetCluster
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type proxyManager interface {
|
|||||||
GetActiveClusterAddresses(ctx context.Context) ([]string, error)
|
GetActiveClusterAddresses(ctx context.Context) ([]string, error)
|
||||||
ClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
ClusterSupportsCustomPorts(ctx context.Context, clusterAddr string) *bool
|
||||||
ClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
ClusterRequireSubdomain(ctx context.Context, clusterAddr string) *bool
|
||||||
|
ClusterSupportsCrowdSec(ctx context.Context, clusterAddr string) *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
@@ -76,6 +77,7 @@ func (m Manager) GetDomains(ctx context.Context, accountID, userID string) ([]*d
|
|||||||
}
|
}
|
||||||
d.SupportsCustomPorts = m.proxyManager.ClusterSupportsCustomPorts(ctx, cluster)
|
d.SupportsCustomPorts = m.proxyManager.ClusterSupportsCustomPorts(ctx, cluster)
|
||||||
d.RequireSubdomain = m.proxyManager.ClusterRequireSubdomain(ctx, cluster)
|
d.RequireSubdomain = m.proxyManager.ClusterRequireSubdomain(ctx, cluster)
|
||||||
|
d.SupportsCrowdSec = m.proxyManager.ClusterSupportsCrowdSec(ctx, cluster)
|
||||||
ret = append(ret, d)
|
ret = append(ret, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +93,7 @@ func (m Manager) GetDomains(ctx context.Context, accountID, userID string) ([]*d
|
|||||||
}
|
}
|
||||||
if d.TargetCluster != "" {
|
if d.TargetCluster != "" {
|
||||||
cd.SupportsCustomPorts = m.proxyManager.ClusterSupportsCustomPorts(ctx, d.TargetCluster)
|
cd.SupportsCustomPorts = m.proxyManager.ClusterSupportsCustomPorts(ctx, d.TargetCluster)
|
||||||
|
cd.SupportsCrowdSec = m.proxyManager.ClusterSupportsCrowdSec(ctx, d.TargetCluster)
|
||||||
}
|
}
|
||||||
// Custom domains never require a subdomain by default since
|
// Custom domains never require a subdomain by default since
|
||||||
// the account owns them and should be able to use the bare domain.
|
// the account owns them and should be able to use the bare domain.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user