mirror of
https://github.com/fosrl/newt.git
synced 2026-02-24 22:06:38 +00:00
Merge branch 'dev' into msg-delivery
This commit is contained in:
6
.github/workflows/cicd.yml
vendored
6
.github/workflows/cicd.yml
vendored
@@ -11,7 +11,9 @@ permissions:
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "[0-9]+.[0-9]+.[0-9]+"
|
||||||
|
- "[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
@@ -273,7 +275,7 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{version}},value=${{ env.TAG }}
|
type=semver,pattern={{version}},value=${{ env.TAG }}
|
||||||
type=semver,pattern={{major}}.{{minor}},value=${{ env.TAG }},enable=${{ env.PUBLISH_MINOR == 'true' && env.IS_RC != 'true' }}
|
type=semver,pattern={{major}}.{{minor}},value=${{ env.TAG }},enable=${{ env.PUBLISH_MINOR == 'true' && env.IS_RC != 'true' }}
|
||||||
type=raw,value=latest,enable=${{ env.PUBLISH_LATEST == 'true' && env.IS_RC != 'true' }}
|
type=raw,value=latest,enable=${{ env.IS_RC != 'true' }}
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=false
|
latest=false
|
||||||
labels: |
|
labels: |
|
||||||
|
|||||||
23
.github/workflows/nix-build.yml
vendored
Normal file
23
.github/workflows/nix-build.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: Build Nix package
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
nix-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
|
- name: Build flake package
|
||||||
|
run: |
|
||||||
|
nix build .#pangolin-newt -L
|
||||||
48
.github/workflows/nix-dependabot-update-hash.yml
vendored
Normal file
48
.github/workflows/nix-dependabot-update-hash.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: Update Nix Package Hash On Dependabot PRs
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
nix-update:
|
||||||
|
if: github.actor == 'dependabot[bot]'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
ref: ${{ github.head_ref }}
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
|
||||||
|
- name: Run nix-update
|
||||||
|
run: |
|
||||||
|
nix run nixpkgs#nix-update -- --flake pangolin-newt --no-src --version skip
|
||||||
|
|
||||||
|
- name: Check for changes
|
||||||
|
id: changes
|
||||||
|
run: |
|
||||||
|
if git diff --quiet; then
|
||||||
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Commit and push changes
|
||||||
|
if: steps.changes.outputs.changed == 'true'
|
||||||
|
run: |
|
||||||
|
git config user.name "dependabot[bot]"
|
||||||
|
git config user.email "dependabot[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
git add .
|
||||||
|
git commit -m "chore(nix): fix hash for updated go dependencies"
|
||||||
|
git push
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,4 +5,6 @@ nohup.out
|
|||||||
*.iml
|
*.iml
|
||||||
certs/
|
certs/
|
||||||
newt_arm64
|
newt_arm64
|
||||||
key
|
key
|
||||||
|
/.direnv/
|
||||||
|
/result*
|
||||||
|
|||||||
@@ -523,7 +523,7 @@ func (b *SharedBind) receiveIPv4Simple(conn *net.UDPConn, bufs [][]byte, sizes [
|
|||||||
func (b *SharedBind) handleMagicPacket(data []byte, addr *net.UDPAddr) bool {
|
func (b *SharedBind) handleMagicPacket(data []byte, addr *net.UDPAddr) bool {
|
||||||
// Check if this is a test request packet
|
// Check if this is a test request packet
|
||||||
if len(data) >= MagicTestRequestLen && bytes.HasPrefix(data, MagicTestRequest) {
|
if len(data) >= MagicTestRequestLen && bytes.HasPrefix(data, MagicTestRequest) {
|
||||||
logger.Debug("Received magic test REQUEST from %s, sending response", addr.String())
|
// logger.Debug("Received magic test REQUEST from %s, sending response", addr.String())
|
||||||
// Extract the random data portion to echo back
|
// Extract the random data portion to echo back
|
||||||
echoData := data[len(MagicTestRequest) : len(MagicTestRequest)+MagicPacketDataLen]
|
echoData := data[len(MagicTestRequest) : len(MagicTestRequest)+MagicPacketDataLen]
|
||||||
|
|
||||||
@@ -546,7 +546,7 @@ func (b *SharedBind) handleMagicPacket(data []byte, addr *net.UDPAddr) bool {
|
|||||||
|
|
||||||
// Check if this is a test response packet
|
// Check if this is a test response packet
|
||||||
if len(data) >= MagicTestResponseLen && bytes.HasPrefix(data, MagicTestResponse) {
|
if len(data) >= MagicTestResponseLen && bytes.HasPrefix(data, MagicTestResponse) {
|
||||||
logger.Debug("Received magic test RESPONSE from %s", addr.String())
|
// logger.Debug("Received magic test RESPONSE from %s", addr.String())
|
||||||
// Extract the echoed data
|
// Extract the echoed data
|
||||||
echoData := data[len(MagicTestResponse) : len(MagicTestResponse)+MagicPacketDataLen]
|
echoData := data[len(MagicTestResponse) : len(MagicTestResponse)+MagicPacketDataLen]
|
||||||
|
|
||||||
|
|||||||
8
clients/permissions/permissions_android.go
Normal file
8
clients/permissions/permissions_android.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build android
|
||||||
|
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
// CheckNativeInterfacePermissions always allows permission on Android.
|
||||||
|
func CheckNativeInterfacePermissions() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build darwin
|
//go:build darwin && !ios
|
||||||
|
|
||||||
package permissions
|
package permissions
|
||||||
|
|
||||||
|
|||||||
8
clients/permissions/permissions_ios.go
Normal file
8
clients/permissions/permissions_ios.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build ios
|
||||||
|
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
// CheckNativeInterfacePermissions always allows permission on iOS.
|
||||||
|
func CheckNativeInterfacePermissions() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build linux
|
//go:build linux && !android
|
||||||
|
|
||||||
package permissions
|
package permissions
|
||||||
|
|
||||||
|
|||||||
15
flake.nix
15
flake.nix
@@ -25,7 +25,7 @@
|
|||||||
inherit (pkgs) lib;
|
inherit (pkgs) lib;
|
||||||
|
|
||||||
# Update version when releasing
|
# Update version when releasing
|
||||||
version = "1.7.0";
|
version = "1.8.0";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
default = self.packages.${system}.pangolin-newt;
|
default = self.packages.${system}.pangolin-newt;
|
||||||
@@ -37,14 +37,26 @@
|
|||||||
|
|
||||||
vendorHash = "sha256-5Xr6mwPtsqEliKeKv2rhhp6JC7u3coP4nnhIxGMqccU=";
|
vendorHash = "sha256-5Xr6mwPtsqEliKeKv2rhhp6JC7u3coP4nnhIxGMqccU=";
|
||||||
|
|
||||||
|
nativeInstallCheckInputs = [ pkgs.versionCheckHook ];
|
||||||
|
|
||||||
env = {
|
env = {
|
||||||
CGO_ENABLED = 0;
|
CGO_ENABLED = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
ldflags = [
|
ldflags = [
|
||||||
|
"-s"
|
||||||
|
"-w"
|
||||||
"-X main.newtVersion=${version}"
|
"-X main.newtVersion=${version}"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# Tests are broken due to a lack of Internet.
|
||||||
|
# Disable running `go test`, and instead do
|
||||||
|
# a simple version check instead.
|
||||||
|
doCheck = false;
|
||||||
|
doInstallCheck = true;
|
||||||
|
|
||||||
|
versionCheckProgramArg = [ "-version" ];
|
||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description = "A tunneling client for Pangolin";
|
description = "A tunneling client for Pangolin";
|
||||||
homepage = "https://github.com/fosrl/newt";
|
homepage = "https://github.com/fosrl/newt";
|
||||||
@@ -52,6 +64,7 @@
|
|||||||
maintainers = [
|
maintainers = [
|
||||||
lib.maintainers.water-sucks
|
lib.maintainers.water-sucks
|
||||||
];
|
];
|
||||||
|
mainProgram = "newt";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ type Target struct {
|
|||||||
timer *time.Timer
|
timer *time.Timer
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
|
client *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusChangeCallback is called when any target's status changes
|
// StatusChangeCallback is called when any target's status changes
|
||||||
@@ -185,6 +186,16 @@ func (m *Monitor) addTargetUnsafe(config Config) error {
|
|||||||
Status: StatusUnknown,
|
Status: StatusUnknown,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
|
client: &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
// Configure TLS settings based on certificate enforcement
|
||||||
|
InsecureSkipVerify: !m.enforceCert,
|
||||||
|
// Use SNI TLS header if present
|
||||||
|
ServerName: config.TLSServerName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
m.targets[config.ID] = target
|
m.targets[config.ID] = target
|
||||||
@@ -378,17 +389,6 @@ func (m *Monitor) performHealthCheck(target *Target) {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(target.Config.Timeout)*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(target.Config.Timeout)*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
// Configure TLS settings based on certificate enforcement
|
|
||||||
InsecureSkipVerify: !m.enforceCert,
|
|
||||||
// Use SNI TLS header if present
|
|
||||||
ServerName: target.Config.TLSServerName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, target.Config.Method, url, nil)
|
req, err := http.NewRequestWithContext(ctx, target.Config.Method, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
target.Status = StatusUnhealthy
|
target.Status = StatusUnhealthy
|
||||||
@@ -408,7 +408,7 @@ func (m *Monitor) performHealthCheck(target *Target) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perform request
|
// Perform request
|
||||||
resp, err := client.Do(req)
|
resp, err := target.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
target.Status = StatusUnhealthy
|
target.Status = StatusUnhealthy
|
||||||
target.LastError = fmt.Sprintf("request failed: %v", err)
|
target.LastError = fmt.Sprintf("request failed: %v", err)
|
||||||
|
|||||||
@@ -38,21 +38,29 @@ type Manager struct {
|
|||||||
exitNodes map[string]ExitNode // key is endpoint
|
exitNodes map[string]ExitNode // key is endpoint
|
||||||
updateChan chan struct{} // signals the goroutine to refresh exit nodes
|
updateChan chan struct{} // signals the goroutine to refresh exit nodes
|
||||||
|
|
||||||
sendHolepunchInterval time.Duration
|
sendHolepunchInterval time.Duration
|
||||||
|
sendHolepunchIntervalMin time.Duration
|
||||||
|
sendHolepunchIntervalMax time.Duration
|
||||||
|
defaultIntervalMin time.Duration
|
||||||
|
defaultIntervalMax time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendHolepunchIntervalMax = 60 * time.Second
|
const defaultSendHolepunchIntervalMax = 60 * time.Second
|
||||||
const sendHolepunchIntervalMin = 1 * time.Second
|
const defaultSendHolepunchIntervalMin = 1 * time.Second
|
||||||
|
|
||||||
// NewManager creates a new hole punch manager
|
// NewManager creates a new hole punch manager
|
||||||
func NewManager(sharedBind *bind.SharedBind, ID string, clientType string, publicKey string) *Manager {
|
func NewManager(sharedBind *bind.SharedBind, ID string, clientType string, publicKey string) *Manager {
|
||||||
return &Manager{
|
return &Manager{
|
||||||
sharedBind: sharedBind,
|
sharedBind: sharedBind,
|
||||||
ID: ID,
|
ID: ID,
|
||||||
clientType: clientType,
|
clientType: clientType,
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
exitNodes: make(map[string]ExitNode),
|
exitNodes: make(map[string]ExitNode),
|
||||||
sendHolepunchInterval: sendHolepunchIntervalMin,
|
sendHolepunchInterval: defaultSendHolepunchIntervalMin,
|
||||||
|
sendHolepunchIntervalMin: defaultSendHolepunchIntervalMin,
|
||||||
|
sendHolepunchIntervalMax: defaultSendHolepunchIntervalMax,
|
||||||
|
defaultIntervalMin: defaultSendHolepunchIntervalMin,
|
||||||
|
defaultIntervalMax: defaultSendHolepunchIntervalMax,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,17 +208,46 @@ func (m *Manager) GetExitNodes() []ExitNode {
|
|||||||
return nodes
|
return nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResetInterval resets the hole punch interval back to the minimum value,
|
// SetServerHolepunchInterval sets custom min and max intervals for hole punching.
|
||||||
// allowing it to climb back up through exponential backoff.
|
// This is useful for low power mode where longer intervals are desired.
|
||||||
// This is useful when network conditions change or connectivity is restored.
|
func (m *Manager) SetServerHolepunchInterval(min, max time.Duration) {
|
||||||
func (m *Manager) ResetInterval() {
|
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
if m.sendHolepunchInterval != sendHolepunchIntervalMin {
|
m.sendHolepunchIntervalMin = min
|
||||||
m.sendHolepunchInterval = sendHolepunchIntervalMin
|
m.sendHolepunchIntervalMax = max
|
||||||
logger.Info("Reset hole punch interval to minimum (%v)", sendHolepunchIntervalMin)
|
m.sendHolepunchInterval = min
|
||||||
|
|
||||||
|
logger.Info("Set hole punch intervals: min=%v, max=%v", min, max)
|
||||||
|
|
||||||
|
// Signal the goroutine to apply the new interval if running
|
||||||
|
if m.running && m.updateChan != nil {
|
||||||
|
select {
|
||||||
|
case m.updateChan <- struct{}{}:
|
||||||
|
default:
|
||||||
|
// Channel full or closed, skip
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInterval returns the current min and max intervals
|
||||||
|
func (m *Manager) GetServerHolepunchInterval() (min, max time.Duration) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
return m.sendHolepunchIntervalMin, m.sendHolepunchIntervalMax
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetServerHolepunchInterval resets the hole punch interval back to the default values.
|
||||||
|
// This restores normal operation after low power mode or other custom settings.
|
||||||
|
func (m *Manager) ResetServerHolepunchInterval() {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
m.sendHolepunchIntervalMin = m.defaultIntervalMin
|
||||||
|
m.sendHolepunchIntervalMax = m.defaultIntervalMax
|
||||||
|
m.sendHolepunchInterval = m.defaultIntervalMin
|
||||||
|
|
||||||
|
logger.Info("Reset hole punch intervals to defaults: min=%v, max=%v", m.defaultIntervalMin, m.defaultIntervalMax)
|
||||||
|
|
||||||
// Signal the goroutine to apply the new interval if running
|
// Signal the goroutine to apply the new interval if running
|
||||||
if m.running && m.updateChan != nil {
|
if m.running && m.updateChan != nil {
|
||||||
@@ -393,7 +430,7 @@ func (m *Manager) runMultipleExitNodes() {
|
|||||||
|
|
||||||
// Start with minimum interval
|
// Start with minimum interval
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
m.sendHolepunchInterval = sendHolepunchIntervalMin
|
m.sendHolepunchInterval = m.sendHolepunchIntervalMin
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
ticker := time.NewTicker(m.sendHolepunchInterval)
|
ticker := time.NewTicker(m.sendHolepunchInterval)
|
||||||
@@ -415,7 +452,7 @@ func (m *Manager) runMultipleExitNodes() {
|
|||||||
}
|
}
|
||||||
// Reset interval to minimum on update
|
// Reset interval to minimum on update
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
m.sendHolepunchInterval = sendHolepunchIntervalMin
|
m.sendHolepunchInterval = m.sendHolepunchIntervalMin
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
ticker.Reset(m.sendHolepunchInterval)
|
ticker.Reset(m.sendHolepunchInterval)
|
||||||
// Send immediate hole punch to newly resolved nodes
|
// Send immediate hole punch to newly resolved nodes
|
||||||
@@ -435,8 +472,8 @@ func (m *Manager) runMultipleExitNodes() {
|
|||||||
// Exponential backoff: double the interval up to max
|
// Exponential backoff: double the interval up to max
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
newInterval := m.sendHolepunchInterval * 2
|
newInterval := m.sendHolepunchInterval * 2
|
||||||
if newInterval > sendHolepunchIntervalMax {
|
if newInterval > m.sendHolepunchIntervalMax {
|
||||||
newInterval = sendHolepunchIntervalMax
|
newInterval = m.sendHolepunchIntervalMax
|
||||||
}
|
}
|
||||||
if newInterval != m.sendHolepunchInterval {
|
if newInterval != m.sendHolepunchInterval {
|
||||||
m.sendHolepunchInterval = newInterval
|
m.sendHolepunchInterval = newInterval
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ func DefaultTestOptions() TestConnectionOptions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cachedAddr holds a cached resolved UDP address
|
||||||
|
type cachedAddr struct {
|
||||||
|
addr *net.UDPAddr
|
||||||
|
resolvedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
// HolepunchTester monitors holepunch connectivity using magic packets
|
// HolepunchTester monitors holepunch connectivity using magic packets
|
||||||
type HolepunchTester struct {
|
type HolepunchTester struct {
|
||||||
sharedBind *bind.SharedBind
|
sharedBind *bind.SharedBind
|
||||||
@@ -53,6 +59,11 @@ type HolepunchTester struct {
|
|||||||
|
|
||||||
// Callback when connection status changes
|
// Callback when connection status changes
|
||||||
callback HolepunchStatusCallback
|
callback HolepunchStatusCallback
|
||||||
|
|
||||||
|
// Address cache to avoid repeated DNS/UDP resolution
|
||||||
|
addrCache map[string]*cachedAddr
|
||||||
|
addrCacheMu sync.RWMutex
|
||||||
|
addrCacheTTL time.Duration // How long cached addresses are valid
|
||||||
}
|
}
|
||||||
|
|
||||||
// HolepunchStatus represents the status of a holepunch connection
|
// HolepunchStatus represents the status of a holepunch connection
|
||||||
@@ -75,7 +86,9 @@ type pendingRequest struct {
|
|||||||
// NewHolepunchTester creates a new holepunch tester using the given SharedBind
|
// NewHolepunchTester creates a new holepunch tester using the given SharedBind
|
||||||
func NewHolepunchTester(sharedBind *bind.SharedBind) *HolepunchTester {
|
func NewHolepunchTester(sharedBind *bind.SharedBind) *HolepunchTester {
|
||||||
return &HolepunchTester{
|
return &HolepunchTester{
|
||||||
sharedBind: sharedBind,
|
sharedBind: sharedBind,
|
||||||
|
addrCache: make(map[string]*cachedAddr),
|
||||||
|
addrCacheTTL: 5 * time.Minute, // Cache addresses for 5 minutes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,12 +148,70 @@ func (t *HolepunchTester) Stop() {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Clear address cache
|
||||||
|
t.addrCacheMu.Lock()
|
||||||
|
t.addrCache = make(map[string]*cachedAddr)
|
||||||
|
t.addrCacheMu.Unlock()
|
||||||
|
|
||||||
logger.Debug("HolepunchTester stopped")
|
logger.Debug("HolepunchTester stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveEndpoint resolves an endpoint to a UDP address, using cache when possible
|
||||||
|
func (t *HolepunchTester) resolveEndpoint(endpoint string) (*net.UDPAddr, error) {
|
||||||
|
// Check cache first
|
||||||
|
t.addrCacheMu.RLock()
|
||||||
|
cached, ok := t.addrCache[endpoint]
|
||||||
|
ttl := t.addrCacheTTL
|
||||||
|
t.addrCacheMu.RUnlock()
|
||||||
|
|
||||||
|
if ok && time.Since(cached.resolvedAt) < ttl {
|
||||||
|
return cached.addr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the endpoint
|
||||||
|
host, err := util.ResolveDomain(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
host = endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err = net.SplitHostPort(host)
|
||||||
|
if err != nil {
|
||||||
|
host = net.JoinHostPort(host, "21820")
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteAddr, err := net.ResolveUDPAddr("udp", host)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to resolve UDP address %s: %w", host, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
t.addrCacheMu.Lock()
|
||||||
|
t.addrCache[endpoint] = &cachedAddr{
|
||||||
|
addr: remoteAddr,
|
||||||
|
resolvedAt: time.Now(),
|
||||||
|
}
|
||||||
|
t.addrCacheMu.Unlock()
|
||||||
|
|
||||||
|
return remoteAddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidateCache removes a specific endpoint from the address cache
|
||||||
|
func (t *HolepunchTester) InvalidateCache(endpoint string) {
|
||||||
|
t.addrCacheMu.Lock()
|
||||||
|
delete(t.addrCache, endpoint)
|
||||||
|
t.addrCacheMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearCache clears all cached addresses
|
||||||
|
func (t *HolepunchTester) ClearCache() {
|
||||||
|
t.addrCacheMu.Lock()
|
||||||
|
t.addrCache = make(map[string]*cachedAddr)
|
||||||
|
t.addrCacheMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// handleResponse is called by SharedBind when a magic response is received
|
// handleResponse is called by SharedBind when a magic response is received
|
||||||
func (t *HolepunchTester) handleResponse(addr netip.AddrPort, echoData []byte) {
|
func (t *HolepunchTester) handleResponse(addr netip.AddrPort, echoData []byte) {
|
||||||
logger.Debug("Received magic response from %s", addr.String())
|
// logger.Debug("Received magic response from %s", addr.String())
|
||||||
key := string(echoData)
|
key := string(echoData)
|
||||||
|
|
||||||
value, ok := t.pendingRequests.LoadAndDelete(key)
|
value, ok := t.pendingRequests.LoadAndDelete(key)
|
||||||
@@ -152,7 +223,7 @@ func (t *HolepunchTester) handleResponse(addr netip.AddrPort, echoData []byte) {
|
|||||||
|
|
||||||
req := value.(*pendingRequest)
|
req := value.(*pendingRequest)
|
||||||
rtt := time.Since(req.sentAt)
|
rtt := time.Since(req.sentAt)
|
||||||
logger.Debug("Magic response matched pending request for %s (RTT: %v)", req.endpoint, rtt)
|
// logger.Debug("Magic response matched pending request for %s (RTT: %v)", req.endpoint, rtt)
|
||||||
|
|
||||||
// Send RTT to the waiting goroutine (non-blocking)
|
// Send RTT to the waiting goroutine (non-blocking)
|
||||||
select {
|
select {
|
||||||
@@ -183,20 +254,10 @@ func (t *HolepunchTester) TestEndpoint(endpoint string, timeout time.Duration) T
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the endpoint
|
// Resolve the endpoint (using cache)
|
||||||
host, err := util.ResolveDomain(endpoint)
|
remoteAddr, err := t.resolveEndpoint(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
host = endpoint
|
result.Error = err
|
||||||
}
|
|
||||||
|
|
||||||
_, _, err = net.SplitHostPort(host)
|
|
||||||
if err != nil {
|
|
||||||
host = net.JoinHostPort(host, "21820")
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteAddr, err := net.ResolveUDPAddr("udp", host)
|
|
||||||
if err != nil {
|
|
||||||
result.Error = fmt.Errorf("failed to resolve UDP address %s: %w", host, err)
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,9 +44,13 @@ func ConfigureInterface(interfaceName string, tunnelIp string, mtu int) error {
|
|||||||
return configureDarwin(interfaceName, ip, ipNet)
|
return configureDarwin(interfaceName, ip, ipNet)
|
||||||
case "windows":
|
case "windows":
|
||||||
return configureWindows(interfaceName, ip, ipNet)
|
return configureWindows(interfaceName, ip, ipNet)
|
||||||
default:
|
case "android":
|
||||||
return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
|
return nil
|
||||||
|
case "ios":
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// waitForInterfaceUp polls the network interface until it's up or times out
|
// waitForInterfaceUp polls the network interface until it's up or times out
|
||||||
|
|||||||
@@ -126,13 +126,14 @@ func LinuxRemoveRoute(destination string) error {
|
|||||||
|
|
||||||
// addRouteForServerIP adds an OS-specific route for the server IP
|
// addRouteForServerIP adds an OS-specific route for the server IP
|
||||||
func AddRouteForServerIP(serverIP, interfaceName string) error {
|
func AddRouteForServerIP(serverIP, interfaceName string) error {
|
||||||
if err := AddRouteForNetworkConfig(serverIP); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if interfaceName == "" {
|
if interfaceName == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if runtime.GOOS == "darwin" {
|
// TODO: does this also need to be ios?
|
||||||
|
if runtime.GOOS == "darwin" { // macos requires routes for each peer to be added but this messes with other platforms
|
||||||
|
if err := AddRouteForNetworkConfig(serverIP); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return DarwinAddRoute(serverIP, "", interfaceName)
|
return DarwinAddRoute(serverIP, "", interfaceName)
|
||||||
}
|
}
|
||||||
// else if runtime.GOOS == "windows" {
|
// else if runtime.GOOS == "windows" {
|
||||||
@@ -145,13 +146,14 @@ func AddRouteForServerIP(serverIP, interfaceName string) error {
|
|||||||
|
|
||||||
// removeRouteForServerIP removes an OS-specific route for the server IP
|
// removeRouteForServerIP removes an OS-specific route for the server IP
|
||||||
func RemoveRouteForServerIP(serverIP string, interfaceName string) error {
|
func RemoveRouteForServerIP(serverIP string, interfaceName string) error {
|
||||||
if err := RemoveRouteForNetworkConfig(serverIP); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if interfaceName == "" {
|
if interfaceName == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if runtime.GOOS == "darwin" {
|
// TODO: does this also need to be ios?
|
||||||
|
if runtime.GOOS == "darwin" { // macos requires routes for each peer to be added but this messes with other platforms
|
||||||
|
if err := RemoveRouteForNetworkConfig(serverIP); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return DarwinRemoveRoute(serverIP)
|
return DarwinRemoveRoute(serverIP)
|
||||||
}
|
}
|
||||||
// else if runtime.GOOS == "windows" {
|
// else if runtime.GOOS == "windows" {
|
||||||
@@ -217,21 +219,22 @@ func AddRoutes(remoteSubnets []string, interfaceName string) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if runtime.GOOS == "darwin" {
|
switch runtime.GOOS {
|
||||||
|
case "darwin":
|
||||||
if err := DarwinAddRoute(subnet, "", interfaceName); err != nil {
|
if err := DarwinAddRoute(subnet, "", interfaceName); err != nil {
|
||||||
logger.Error("Failed to add Darwin route for subnet %s: %v", subnet, err)
|
logger.Error("Failed to add Darwin route for subnet %s: %v", subnet, err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
} else if runtime.GOOS == "windows" {
|
case "windows":
|
||||||
if err := WindowsAddRoute(subnet, "", interfaceName); err != nil {
|
if err := WindowsAddRoute(subnet, "", interfaceName); err != nil {
|
||||||
logger.Error("Failed to add Windows route for subnet %s: %v", subnet, err)
|
logger.Error("Failed to add Windows route for subnet %s: %v", subnet, err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
} else if runtime.GOOS == "linux" {
|
case "linux":
|
||||||
if err := LinuxAddRoute(subnet, "", interfaceName); err != nil {
|
if err := LinuxAddRoute(subnet, "", interfaceName); err != nil {
|
||||||
logger.Error("Failed to add Linux route for subnet %s: %v", subnet, err)
|
logger.Error("Failed to add Linux route for subnet %s: %v", subnet, err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
case "android", "ios":
|
||||||
|
// Routes handled by the OS/VPN service
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Added route for remote subnet: %s", subnet)
|
logger.Info("Added route for remote subnet: %s", subnet)
|
||||||
@@ -258,21 +261,22 @@ func RemoveRoutes(remoteSubnets []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Remove route based on operating system
|
// Remove route based on operating system
|
||||||
if runtime.GOOS == "darwin" {
|
switch runtime.GOOS {
|
||||||
|
case "darwin":
|
||||||
if err := DarwinRemoveRoute(subnet); err != nil {
|
if err := DarwinRemoveRoute(subnet); err != nil {
|
||||||
logger.Error("Failed to remove Darwin route for subnet %s: %v", subnet, err)
|
logger.Error("Failed to remove Darwin route for subnet %s: %v", subnet, err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
} else if runtime.GOOS == "windows" {
|
case "windows":
|
||||||
if err := WindowsRemoveRoute(subnet); err != nil {
|
if err := WindowsRemoveRoute(subnet); err != nil {
|
||||||
logger.Error("Failed to remove Windows route for subnet %s: %v", subnet, err)
|
logger.Error("Failed to remove Windows route for subnet %s: %v", subnet, err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
} else if runtime.GOOS == "linux" {
|
case "linux":
|
||||||
if err := LinuxRemoveRoute(subnet); err != nil {
|
if err := LinuxRemoveRoute(subnet); err != nil {
|
||||||
logger.Error("Failed to remove Linux route for subnet %s: %v", subnet, err)
|
logger.Error("Failed to remove Linux route for subnet %s: %v", subnet, err)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
case "android", "ios":
|
||||||
|
// Routes handled by the OS/VPN service
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Removed route for remote subnet: %s", subnet)
|
logger.Info("Removed route for remote subnet: %s", subnet)
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ func RemoveIPv4IncludedRoute(route IPv4Route) {
|
|||||||
if r == route {
|
if r == route {
|
||||||
networkSettings.IPv4IncludedRoutes = append(routes[:i], routes[i+1:]...)
|
networkSettings.IPv4IncludedRoutes = append(routes[:i], routes[i+1:]...)
|
||||||
logger.Info("Removed IPv4 included route: %+v", route)
|
logger.Info("Removed IPv4 included route: %+v", route)
|
||||||
return
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
incrementor++
|
incrementor++
|
||||||
|
|||||||
Reference in New Issue
Block a user