Compare commits

...

42 Commits

Author SHA1 Message Date
Owen
d754cea397 Dont run on v tags 2025-12-23 17:54:31 -05:00
Owen
e1ee4dc8f2 Fix latest tag 2025-12-22 21:32:47 -05:00
Varun Narravula
f9b6f36b4f ci: update nix go vendor hash if needed for dependabot PRs 2025-12-22 19:43:48 -05:00
Varun Narravula
0e961761b8 chore: add direnv and nix result dirs to gitignore 2025-12-22 19:43:48 -05:00
Varun Narravula
baf1b9b972 ci: build nix package when go.mod is changed 2025-12-22 19:43:48 -05:00
Varun Narravula
f078136b5a fix(nix): disable tests, set meta.mainProgram for package 2025-12-22 19:43:48 -05:00
Varun Narravula
ca341a8bb0 chore(nix): sync version number with latest version 2025-12-22 19:43:48 -05:00
Owen
80ae03997a Merge branch 'dev' 2025-12-22 16:15:41 -05:00
Owen
5c94789d9a Quiet up logs 2025-12-22 14:31:44 -05:00
Owen
6c65cc8e5e Fix makefile cicd binaries 2025-12-21 21:34:56 -05:00
Owen
a21a8e90fa Add back release and binaries 2025-12-21 21:01:04 -05:00
Owen
3d5335f2cb Add back release and binaries 2025-12-21 21:00:45 -05:00
Owen Schwartz
94788edce3 Merge pull request #214 from fosrl/dev
1.8.0-rc.0
2025-12-21 20:59:32 -05:00
Owen
2bbe037544 Merge branch 'main' into dev 2025-12-21 20:57:45 -05:00
Owen
9b015e9f7c Tie siteIds to exit node 2025-12-19 10:54:21 -05:00
Owen
3305f711b9 Prevent sigsegv with bad address
Fixes #210
Fixes #201
2025-12-18 10:29:37 -05:00
Owen
ff7fe1275b Take 21820 from config 2025-12-16 18:35:25 -05:00
Owen
1cbf41e094 Take 21820 from config 2025-12-16 18:33:05 -05:00
Owen Schwartz
9bc35433ef Merge pull request #208 from fosrl/icmp2
Support ICMP test requests for clients
2025-12-16 17:19:22 -05:00
Owen
b8349aab4e Install iputils not ping 2025-12-16 17:16:58 -05:00
Owen
3f29a553ae Merge branch 'dev' into icmp2 2025-12-16 17:15:15 -05:00
Owen
745045f619 Merge branch 'main' into dev 2025-12-16 17:15:06 -05:00
Owen
3783a12055 Add fallback to non privileged ping 2025-12-16 17:05:36 -05:00
Owen
a9b84c8c09 Disabling icmp ping 2025-12-16 16:30:14 -05:00
Owen
5c5ef4c7e6 Merge branch 'dev' into icmp2 2025-12-16 13:48:00 -05:00
Owen
6e9249e664 Add disable icmp 2025-12-16 13:47:45 -05:00
Owen
55be2a52a5 Handle reply correctly 2025-12-16 12:23:12 -05:00
Owen
058330d41b Icmp2 2025-12-16 12:05:59 -05:00
Owen Schwartz
5e7b970115 Merge pull request #203 from fosrl/port-firewall
Port firewalling for Private Resources
2025-12-15 22:16:02 -05:00
Owen
dc180abba9 Add test udp server and client 2025-12-15 22:11:57 -05:00
Owen
004bb9b12d Allow proto restriction 2025-12-15 18:37:34 -05:00
Owen
0637360b31 Fix healthcheck interval not resetting
Ref PAN-158
2025-12-15 12:10:47 -05:00
Owen Schwartz
d5e0771094 Merge pull request #200 from water-sucks/parallelize-makefile
feat(build): parallelize go-build-release and github actions with matrix
2025-12-13 09:51:59 -05:00
Varun Narravula
1dcb68d694 feat(ci): use matrix for building all makefile targets in parallel 2025-12-12 15:51:53 -08:00
Varun Narravula
865ac4b682 feat(build): double-tag docker release builds with "latest" and tag name 2025-12-12 15:51:20 -08:00
Varun Narravula
de5627b0b7 feat(build): parallelize go-build-release using separate arch rules 2025-12-12 15:51:15 -08:00
Owen
44470abd54 Print version before otel 2025-12-12 14:32:12 -05:00
Owen
4bb0537c39 Remove accidental file 2025-12-11 23:27:13 -05:00
Owen
92fb96f9bd Fix test 2025-12-11 23:24:14 -05:00
Owen Schwartz
b68b7fe49d Merge pull request #199 from water-sucks/update-nix-hash
fix(nix): use correct hash for vendored deps
2025-12-11 23:21:57 -05:00
Varun Narravula
1da424bb20 feat(nix): sync version number 2025-12-11 17:53:07 -08:00
Varun Narravula
22e5104a41 fix(nix): use correct hash for vendored deps 2025-12-11 17:52:52 -08:00
20 changed files with 892 additions and 4424 deletions

View File

@@ -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: |
@@ -587,28 +589,28 @@ jobs:
# sarif_file: trivy-ghcr.sarif # sarif_file: trivy-ghcr.sarif
# category: Image Vulnerability Scan # category: Image Vulnerability Scan
# - name: Build binaries - name: Build binaries
# env: env:
# CGO_ENABLED: "0" CGO_ENABLED: "0"
# GOFLAGS: "-trimpath" GOFLAGS: "-trimpath"
# run: | run: |
# set -euo pipefail set -euo pipefail
# TAG_VAR="${TAG}" TAG_VAR="${TAG}"
# make go-build-release tag=$TAG_VAR make -j 10 go-build-release tag=$TAG_VAR
# shell: bash shell: bash
# - name: Create GitHub Release - name: Create GitHub Release
# uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
# with: with:
# tag_name: ${{ env.TAG }} tag_name: ${{ env.TAG }}
# generate_release_notes: true generate_release_notes: true
# prerelease: ${{ env.IS_RC == 'true' }} prerelease: ${{ env.IS_RC == 'true' }}
# files: | files: |
# bin/* bin/*
# fail_on_unmatched_files: true fail_on_unmatched_files: true
# draft: true draft: true
# body: | body: |
# ## Container Images ## Container Images
# - GHCR: `${{ env.GHCR_REF }}` - GHCR: `${{ env.GHCR_REF }}`
# - Docker Hub: `${{ env.DH_REF || 'N/A' }}` - Docker Hub: `${{ env.DH_REF || 'N/A' }}`
# **Digest:** `${{ steps.build.outputs.digest }}` **Digest:** `${{ steps.build.outputs.digest }}`

23
.github/workflows/nix-build.yml vendored Normal file
View 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

View 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

View File

@@ -10,22 +10,30 @@ on:
- dev - dev
jobs: jobs:
test: build:
runs-on: amd64-runner runs-on: ubuntu-latest
strategy:
matrix:
target:
- local
- docker-build
- go-build-release-darwin-amd64
- go-build-release-darwin-arm64
- go-build-release-freebsd-amd64
- go-build-release-freebsd-arm64
- go-build-release-linux-amd64
- go-build-release-linux-arm32-v6
- go-build-release-linux-arm32-v7
- go-build-release-linux-riscv64
- go-build-release-windows-amd64
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
with: with:
go-version: 1.25 go-version: 1.25
- name: Build go - name: Build targets via `make`
run: go build run: make ${{ matrix.target }}
- name: Build Docker image
run: make build
- name: Build binaries
run: make go-build-release

2
.gitignore vendored
View File

@@ -6,3 +6,5 @@ nohup.out
certs/ certs/
newt_arm64 newt_arm64
key key
/.direnv/
/result*

View File

@@ -20,7 +20,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /newt
FROM alpine:3.23 AS runner FROM alpine:3.23 AS runner
RUN apk --no-cache add ca-certificates tzdata RUN apk --no-cache add ca-certificates tzdata iputils
COPY --from=builder /newt /usr/local/bin/ COPY --from=builder /newt /usr/local/bin/
COPY entrypoint.sh / COPY entrypoint.sh /

View File

@@ -1,25 +1,70 @@
.PHONY: all local docker-build docker-build-release
all: local all: local
local:
CGO_ENABLED=0 go build -o ./bin/newt
docker-build:
docker build -t fosrl/newt:latest .
docker-build-release: docker-build-release:
@if [ -z "$(tag)" ]; then \ @if [ -z "$(tag)" ]; then \
echo "Error: tag is required. Usage: make docker-build-release tag=<tag>"; \ echo "Error: tag is required. Usage: make docker-build-release tag=<tag>"; \
exit 1; \ exit 1; \
fi fi
docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 -t fosrl/newt:latest -f Dockerfile --push . docker buildx build . \
docker buildx build --platform linux/arm/v7,linux/arm64,linux/amd64 -t fosrl/newt:$(tag) -f Dockerfile --push . --platform linux/arm/v7,linux/arm64,linux/amd64 \
-t fosrl/newt:latest \
-t fosrl/newt:$(tag) \
-f Dockerfile \
--push
local: .PHONY: go-build-release \
CGO_ENABLED=0 go build -o ./bin/newt go-build-release-linux-arm64 go-build-release-linux-arm32-v7 \
go-build-release-linux-arm32-v6 go-build-release-linux-amd64 \
go-build-release-linux-riscv64 go-build-release-darwin-arm64 \
go-build-release-darwin-amd64 go-build-release-windows-amd64 \
go-build-release-freebsd-amd64 go-build-release-freebsd-arm64
go-build-release: go-build-release: \
go-build-release-linux-arm64 \
go-build-release-linux-arm32-v7 \
go-build-release-linux-arm32-v6 \
go-build-release-linux-amd64 \
go-build-release-linux-riscv64 \
go-build-release-darwin-arm64 \
go-build-release-darwin-amd64 \
go-build-release-windows-amd64 \
go-build-release-freebsd-amd64 \
go-build-release-freebsd-arm64
go-build-release-linux-arm64:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/newt_linux_arm64 CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/newt_linux_arm64
go-build-release-linux-arm32-v7:
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o bin/newt_linux_arm32 CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -o bin/newt_linux_arm32
go-build-release-linux-arm32-v6:
CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -o bin/newt_linux_arm32v6 CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -o bin/newt_linux_arm32v6
go-build-release-linux-amd64:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/newt_linux_amd64 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/newt_linux_amd64
go-build-release-linux-riscv64:
CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -o bin/newt_linux_riscv64 CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 go build -o bin/newt_linux_riscv64
go-build-release-darwin-arm64:
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/newt_darwin_arm64 CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o bin/newt_darwin_arm64
go-build-release-darwin-amd64:
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/newt_darwin_amd64 CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/newt_darwin_amd64
go-build-release-windows-amd64:
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/newt_windows_amd64.exe CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/newt_windows_amd64.exe
go-build-release-freebsd-amd64:
CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o bin/newt_freebsd_amd64 CGO_ENABLED=0 GOOS=freebsd GOARCH=amd64 go build -o bin/newt_freebsd_amd64
go-build-release-freebsd-arm64:
CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -o bin/newt_freebsd_arm64 CGO_ENABLED=0 GOOS=freebsd GOARCH=arm64 go build -o bin/newt_freebsd_arm64

View File

@@ -24,7 +24,7 @@ func setupClients(client *websocket.Client) {
host = strings.TrimSuffix(host, "/") host = strings.TrimSuffix(host, "/")
logger.Info("Setting up clients with netstack2...") logger.Debug("Setting up clients with netstack2...")
// if useNativeInterface is true make sure we have permission to use native interface // if useNativeInterface is true make sure we have permission to use native interface
if useNativeInterface { if useNativeInterface {
@@ -63,7 +63,7 @@ func closeClients() {
} }
} }
func clientsHandleNewtConnection(publicKey string, endpoint string) { func clientsHandleNewtConnection(publicKey string, endpoint string, relayPort uint16) {
if !ready { if !ready {
return return
} }
@@ -77,7 +77,7 @@ func clientsHandleNewtConnection(publicKey string, endpoint string) {
endpoint = strings.Join(parts[:len(parts)-1], ":") endpoint = strings.Join(parts[:len(parts)-1], ":")
if wgService != nil { if wgService != nil {
wgService.StartHolepunch(publicKey, endpoint) wgService.StartHolepunch(publicKey, endpoint, relayPort)
} }
} }

View File

@@ -40,12 +40,14 @@ type Target struct {
SourcePrefix string `json:"sourcePrefix"` SourcePrefix string `json:"sourcePrefix"`
DestPrefix string `json:"destPrefix"` DestPrefix string `json:"destPrefix"`
RewriteTo string `json:"rewriteTo,omitempty"` RewriteTo string `json:"rewriteTo,omitempty"`
DisableIcmp bool `json:"disableIcmp,omitempty"`
PortRange []PortRange `json:"portRange,omitempty"` PortRange []PortRange `json:"portRange,omitempty"`
} }
type PortRange struct { type PortRange struct {
Min uint16 `json:"min"` Min uint16 `json:"min"`
Max uint16 `json:"max"` Max uint16 `json:"max"`
Protocol string `json:"protocol"` // "tcp" or "udp"
} }
type Peer struct { type Peer struct {
@@ -139,7 +141,7 @@ func NewWireGuardService(interfaceName string, port uint16, mtu int, host string
// Add a reference for the hole punch manager (creator already has one reference for WireGuard) // Add a reference for the hole punch manager (creator already has one reference for WireGuard)
sharedBind.AddRef() sharedBind.AddRef()
logger.Info("Created shared UDP socket on port %d (refcount: %d)", port, sharedBind.GetRefCount()) logger.Debug("Created shared UDP socket on port %d (refcount: %d)", port, sharedBind.GetRefCount())
// Parse DNS addresses // Parse DNS addresses
dnsAddrs := []netip.Addr{netip.MustParseAddr(dns)} dnsAddrs := []netip.Addr{netip.MustParseAddr(dns)}
@@ -268,16 +270,21 @@ func (s *WireGuardService) SetOnNetstackClose(callback func()) {
} }
// StartHolepunch starts hole punching to a specific endpoint // StartHolepunch starts hole punching to a specific endpoint
func (s *WireGuardService) StartHolepunch(publicKey string, endpoint string) { func (s *WireGuardService) StartHolepunch(publicKey string, endpoint string, relayPort uint16) {
if s.holePunchManager == nil { if s.holePunchManager == nil {
logger.Warn("Hole punch manager not initialized") logger.Warn("Hole punch manager not initialized")
return return
} }
if relayPort == 0 {
relayPort = 21820
}
// Convert websocket.ExitNode to holepunch.ExitNode // Convert websocket.ExitNode to holepunch.ExitNode
hpExitNodes := []holepunch.ExitNode{ hpExitNodes := []holepunch.ExitNode{
{ {
Endpoint: endpoint, Endpoint: endpoint,
RelayPort: relayPort,
PublicKey: publicKey, PublicKey: publicKey,
}, },
} }
@@ -287,7 +294,7 @@ func (s *WireGuardService) StartHolepunch(publicKey string, endpoint string) {
logger.Warn("Failed to start hole punch: %v", err) logger.Warn("Failed to start hole punch: %v", err)
} }
logger.Info("Starting hole punch to %s with public key: %s", endpoint, publicKey) logger.Debug("Starting hole punch to %s with public key: %s", endpoint, publicKey)
} }
// StartDirectUDPRelay starts a direct UDP relay from the main tunnel netstack to the clients' WireGuard. // StartDirectUDPRelay starts a direct UDP relay from the main tunnel netstack to the clients' WireGuard.
@@ -334,7 +341,7 @@ func (s *WireGuardService) StartDirectUDPRelay(tunnelIP string) error {
// Set the netstack connection on the SharedBind so responses go back through the tunnel // Set the netstack connection on the SharedBind so responses go back through the tunnel
s.sharedBind.SetNetstackConn(listener) s.sharedBind.SetNetstackConn(listener)
logger.Info("Started direct UDP relay on %s:%d (bidirectional via SharedBind)", tunnelIP, s.Port) logger.Debug("Started direct UDP relay on %s:%d (bidirectional via SharedBind)", tunnelIP, s.Port)
// Start the relay goroutine to read from netstack and inject into SharedBind // Start the relay goroutine to read from netstack and inject into SharedBind
s.directRelayWg.Add(1) s.directRelayWg.Add(1)
@@ -352,7 +359,7 @@ func (s *WireGuardService) runDirectUDPRelay(listener net.PacketConn) {
// Note: Don't close listener here - it's also used by SharedBind for sending responses // Note: Don't close listener here - it's also used by SharedBind for sending responses
// It will be closed when the relay is stopped // It will be closed when the relay is stopped
logger.Info("Direct UDP relay started (bidirectional through SharedBind)") logger.Debug("Direct UDP relay started (bidirectional through SharedBind)")
buf := make([]byte, 65535) // Max UDP packet size buf := make([]byte, 65535) // Max UDP packet size
@@ -438,7 +445,7 @@ func (s *WireGuardService) LoadRemoteConfig() error {
"port": s.Port, "port": s.Port,
}, 2*time.Second) }, 2*time.Second)
logger.Info("Requesting WireGuard configuration from remote server") logger.Debug("Requesting WireGuard configuration from remote server")
go s.periodicBandwidthCheck() go s.periodicBandwidthCheck()
return nil return nil
@@ -448,7 +455,7 @@ func (s *WireGuardService) handleConfig(msg websocket.WSMessage) {
var config WgConfig var config WgConfig
logger.Debug("Received message: %v", msg) logger.Debug("Received message: %v", msg)
logger.Info("Received WireGuard clients configuration from remote server") logger.Debug("Received WireGuard clients configuration from remote server")
jsonData, err := json.Marshal(msg.Data) jsonData, err := json.Marshal(msg.Data)
if err != nil { if err != nil {
@@ -470,6 +477,8 @@ func (s *WireGuardService) handleConfig(msg websocket.WSMessage) {
// Ensure the WireGuard interface and peers are configured // Ensure the WireGuard interface and peers are configured
if err := s.ensureWireguardInterface(config); err != nil { if err := s.ensureWireguardInterface(config); err != nil {
logger.Error("Failed to ensure WireGuard interface: %v", err) logger.Error("Failed to ensure WireGuard interface: %v", err)
logger.Error("Clients functionality will be disabled until the interface can be created")
return
} }
if err := s.ensureWireguardPeers(config.Peers); err != nil { if err := s.ensureWireguardPeers(config.Peers); err != nil {
@@ -479,6 +488,8 @@ func (s *WireGuardService) handleConfig(msg websocket.WSMessage) {
if err := s.ensureTargets(config.Targets); err != nil { if err := s.ensureTargets(config.Targets); err != nil {
logger.Error("Failed to ensure WireGuard targets: %v", err) logger.Error("Failed to ensure WireGuard targets: %v", err)
} }
logger.Info("Client connectivity setup. Ready to accept connections from clients!")
} }
func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error { func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error {
@@ -594,6 +605,7 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error {
netstack2.NetTunOptions{ netstack2.NetTunOptions{
EnableTCPProxy: true, EnableTCPProxy: true,
EnableUDPProxy: true, EnableUDPProxy: true,
EnableICMPProxy: true,
}, },
) )
if err != nil { if err != nil {
@@ -625,7 +637,7 @@ func (s *WireGuardService) ensureWireguardInterface(wgconfig WgConfig) error {
return fmt.Errorf("failed to bring up WireGuard device: %v", err) return fmt.Errorf("failed to bring up WireGuard device: %v", err)
} }
logger.Info("WireGuard netstack device created and configured") logger.Debug("WireGuard netstack device created and configured")
// Release the mutex before calling the callback // Release the mutex before calling the callback
s.mu.Unlock() s.mu.Unlock()
@@ -644,6 +656,11 @@ func (s *WireGuardService) ensureWireguardPeers(peers []Peer) error {
// For netstack, we need to manage peers differently // For netstack, we need to manage peers differently
// We'll configure peers directly on the device using IPC // We'll configure peers directly on the device using IPC
// Check if device is initialized
if s.device == nil {
return fmt.Errorf("WireGuard device is not initialized")
}
// First, clear all existing peers by getting current config and removing them // First, clear all existing peers by getting current config and removing them
currentConfig, err := s.device.IpcGet() currentConfig, err := s.device.IpcGet()
if err != nil { if err != nil {
@@ -701,10 +718,11 @@ func (s *WireGuardService) ensureTargets(targets []Target) error {
portRanges = append(portRanges, netstack2.PortRange{ portRanges = append(portRanges, netstack2.PortRange{
Min: pr.Min, Min: pr.Min,
Max: pr.Max, Max: pr.Max,
Protocol: pr.Protocol,
}) })
} }
s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges) s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges, target.DisableIcmp)
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", target.SourcePrefix, target.DestPrefix, target.RewriteTo, target.PortRange) logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", target.SourcePrefix, target.DestPrefix, target.RewriteTo, target.PortRange)
} }
@@ -1092,10 +1110,11 @@ func (s *WireGuardService) handleAddTarget(msg websocket.WSMessage) {
portRanges = append(portRanges, netstack2.PortRange{ portRanges = append(portRanges, netstack2.PortRange{
Min: pr.Min, Min: pr.Min,
Max: pr.Max, Max: pr.Max,
Protocol: pr.Protocol,
}) })
} }
s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges) s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges, target.DisableIcmp)
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", target.SourcePrefix, target.DestPrefix, target.RewriteTo, target.PortRange) logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", target.SourcePrefix, target.DestPrefix, target.RewriteTo, target.PortRange)
} }
@@ -1209,10 +1228,11 @@ func (s *WireGuardService) handleUpdateTarget(msg websocket.WSMessage) {
portRanges = append(portRanges, netstack2.PortRange{ portRanges = append(portRanges, netstack2.PortRange{
Min: pr.Min, Min: pr.Min,
Max: pr.Max, Max: pr.Max,
Protocol: pr.Protocol,
}) })
} }
s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges) s.tnet.AddProxySubnetRule(sourcePrefix, destPrefix, target.RewriteTo, portRanges, target.DisableIcmp)
logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", target.SourcePrefix, target.DestPrefix, target.RewriteTo, target.PortRange) logger.Info("Added target subnet from %s to %s rewrite to %s with port ranges: %v", target.SourcePrefix, target.DestPrefix, target.RewriteTo, target.PortRange)
} }
} }

View File

@@ -25,7 +25,7 @@
inherit (pkgs) lib; inherit (pkgs) lib;
# Update version when releasing # Update version when releasing
version = "1.6.1"; version = "1.8.0";
in in
{ {
default = self.packages.${system}.pangolin-newt; default = self.packages.${system}.pangolin-newt;
@@ -35,16 +35,28 @@
inherit version; inherit version;
src = pkgs.nix-gitignore.gitignoreSource [ ] ./.; src = pkgs.nix-gitignore.gitignoreSource [ ] ./.;
vendorHash = "sha256-krxkfH+4z0rgmFi8OyCzWIrxU5Rpb7gjtGcn3LnZCig="; 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";
}; };
}; };
} }

View File

@@ -58,7 +58,7 @@ type Target struct {
LastCheck time.Time `json:"lastCheck"` LastCheck time.Time `json:"lastCheck"`
LastError string `json:"lastError,omitempty"` LastError string `json:"lastError,omitempty"`
CheckCount int `json:"checkCount"` CheckCount int `json:"checkCount"`
ticker *time.Ticker timer *time.Timer
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
} }
@@ -304,26 +304,26 @@ func (m *Monitor) monitorTarget(target *Target) {
go m.callback(m.GetTargets()) go m.callback(m.GetTargets())
} }
// Set up ticker based on current status // Set up timer based on current status
interval := time.Duration(target.Config.Interval) * time.Second interval := time.Duration(target.Config.Interval) * time.Second
if target.Status == StatusUnhealthy { if target.Status == StatusUnhealthy {
interval = time.Duration(target.Config.UnhealthyInterval) * time.Second interval = time.Duration(target.Config.UnhealthyInterval) * time.Second
} }
logger.Debug("Target %d: initial check interval set to %v", target.Config.ID, interval) logger.Debug("Target %d: initial check interval set to %v", target.Config.ID, interval)
target.ticker = time.NewTicker(interval) target.timer = time.NewTimer(interval)
defer target.ticker.Stop() defer target.timer.Stop()
for { for {
select { select {
case <-target.ctx.Done(): case <-target.ctx.Done():
logger.Info("Stopping health check monitoring for target %d", target.Config.ID) logger.Info("Stopping health check monitoring for target %d", target.Config.ID)
return return
case <-target.ticker.C: case <-target.timer.C:
oldStatus := target.Status oldStatus := target.Status
m.performHealthCheck(target) m.performHealthCheck(target)
// Update ticker interval if status changed // Update timer interval if status changed
newInterval := time.Duration(target.Config.Interval) * time.Second newInterval := time.Duration(target.Config.Interval) * time.Second
if target.Status == StatusUnhealthy { if target.Status == StatusUnhealthy {
newInterval = time.Duration(target.Config.UnhealthyInterval) * time.Second newInterval = time.Duration(target.Config.UnhealthyInterval) * time.Second
@@ -332,11 +332,12 @@ func (m *Monitor) monitorTarget(target *Target) {
if newInterval != interval { if newInterval != interval {
logger.Debug("Target %d: updating check interval from %v to %v due to status change", logger.Debug("Target %d: updating check interval from %v to %v due to status change",
target.Config.ID, interval, newInterval) target.Config.ID, interval, newInterval)
target.ticker.Stop()
target.ticker = time.NewTicker(newInterval)
interval = newInterval interval = newInterval
} }
// Reset timer for next check with current interval
target.timer.Reset(interval)
// Notify callback if status changed // Notify callback if status changed
if oldStatus != target.Status && m.callback != nil { if oldStatus != target.Status && m.callback != nil {
logger.Info("Target %d status changed: %s -> %s", logger.Info("Target %d status changed: %s -> %s",

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
"strconv"
"sync" "sync"
"time" "time"
@@ -19,7 +20,9 @@ import (
// ExitNode represents a WireGuard exit node for hole punching // ExitNode represents a WireGuard exit node for hole punching
type ExitNode struct { type ExitNode struct {
Endpoint string `json:"endpoint"` Endpoint string `json:"endpoint"`
RelayPort uint16 `json:"relayPort"`
PublicKey string `json:"publicKey"` PublicKey string `json:"publicKey"`
SiteIds []int `json:"siteIds,omitempty"`
} }
// Manager handles UDP hole punching operations // Manager handles UDP hole punching operations
@@ -140,6 +143,51 @@ func (m *Manager) RemoveExitNode(endpoint string) bool {
return true return true
} }
/*
RemoveExitNodesByPeer removes the peer ID from the SiteIds list in each exit node.
If the SiteIds list becomes empty after removal, the exit node is removed entirely.
Returns the number of exit nodes removed.
*/
func (m *Manager) RemoveExitNodesByPeer(peerID int) int {
m.mu.Lock()
defer m.mu.Unlock()
removed := 0
for endpoint, node := range m.exitNodes {
// Remove peerID from SiteIds if present
newSiteIds := make([]int, 0, len(node.SiteIds))
for _, id := range node.SiteIds {
if id != peerID {
newSiteIds = append(newSiteIds, id)
}
}
if len(newSiteIds) != len(node.SiteIds) {
node.SiteIds = newSiteIds
if len(node.SiteIds) == 0 {
delete(m.exitNodes, endpoint)
logger.Info("Removed exit node %s as no more site IDs remain after removing peer %d", endpoint, peerID)
removed++
} else {
m.exitNodes[endpoint] = node
logger.Info("Removed peer %d from exit node %s site IDs", peerID, endpoint)
}
}
}
if removed > 0 {
// Signal the goroutine to refresh if running
if m.running && m.updateChan != nil {
select {
case m.updateChan <- struct{}{}:
default:
// Channel full or closed, skip
}
}
}
return removed
}
// GetExitNodes returns a copy of the current exit nodes // GetExitNodes returns a copy of the current exit nodes
func (m *Manager) GetExitNodes() []ExitNode { func (m *Manager) GetExitNodes() []ExitNode {
m.mu.Lock() m.mu.Lock()
@@ -202,7 +250,7 @@ func (m *Manager) TriggerHolePunch() error {
continue continue
} }
serverAddr := net.JoinHostPort(host, "21820") serverAddr := net.JoinHostPort(host, strconv.Itoa(int(exitNode.RelayPort)))
remoteAddr, err := net.ResolveUDPAddr("udp", serverAddr) remoteAddr, err := net.ResolveUDPAddr("udp", serverAddr)
if err != nil { if err != nil {
logger.Error("Failed to resolve UDP address %s: %v", serverAddr, err) logger.Error("Failed to resolve UDP address %s: %v", serverAddr, err)
@@ -247,7 +295,7 @@ func (m *Manager) StartMultipleExitNodes(exitNodes []ExitNode) error {
m.updateChan = make(chan struct{}, 1) m.updateChan = make(chan struct{}, 1)
m.mu.Unlock() m.mu.Unlock()
logger.Info("Starting UDP hole punch to %d exit nodes with shared bind", len(exitNodes)) logger.Debug("Starting UDP hole punch to %d exit nodes with shared bind", len(exitNodes))
go m.runMultipleExitNodes() go m.runMultipleExitNodes()
@@ -313,7 +361,7 @@ func (m *Manager) runMultipleExitNodes() {
continue continue
} }
serverAddr := net.JoinHostPort(host, "21820") serverAddr := net.JoinHostPort(host, strconv.Itoa(int(exitNode.RelayPort)))
remoteAddr, err := net.ResolveUDPAddr("udp", serverAddr) remoteAddr, err := net.ResolveUDPAddr("udp", serverAddr)
if err != nil { if err != nil {
logger.Error("Failed to resolve UDP address %s: %v", serverAddr, err) logger.Error("Failed to resolve UDP address %s: %v", serverAddr, err)
@@ -325,7 +373,7 @@ func (m *Manager) runMultipleExitNodes() {
publicKey: exitNode.PublicKey, publicKey: exitNode.PublicKey,
endpointName: exitNode.Endpoint, endpointName: exitNode.Endpoint,
}) })
logger.Info("Resolved exit node: %s -> %s", exitNode.Endpoint, remoteAddr.String()) logger.Debug("Resolved exit node: %s -> %s", exitNode.Endpoint, remoteAddr.String())
} }
return resolvedNodes return resolvedNodes
} }

4293
log.log

File diff suppressed because it is too large Load Diff

24
main.go
View File

@@ -37,6 +37,7 @@ import (
type WgData struct { type WgData struct {
Endpoint string `json:"endpoint"` Endpoint string `json:"endpoint"`
RelayPort uint16 `json:"relayPort"`
PublicKey string `json:"publicKey"` PublicKey string `json:"publicKey"`
ServerIP string `json:"serverIP"` ServerIP string `json:"serverIP"`
TunnelIP string `json:"tunnelIP"` TunnelIP string `json:"tunnelIP"`
@@ -388,6 +389,13 @@ func runNewtMain(ctx context.Context) {
tlsClientCAs = append(tlsClientCAs, tlsClientCAsFlag...) tlsClientCAs = append(tlsClientCAs, tlsClientCAsFlag...)
} }
if *version {
fmt.Println("Newt version " + newtVersion)
os.Exit(0)
} else {
logger.Info("Newt version %s", newtVersion)
}
logger.Init(nil) logger.Init(nil)
loggerLevel := util.ParseLogLevel(logLevel) loggerLevel := util.ParseLogLevel(logLevel)
logger.GetLogger().SetLevel(loggerLevel) logger.GetLogger().SetLevel(loggerLevel)
@@ -412,7 +420,7 @@ func runNewtMain(ctx context.Context) {
} }
if tel != nil { if tel != nil {
// Admin HTTP server (exposes /metrics when Prometheus exporter is enabled) // Admin HTTP server (exposes /metrics when Prometheus exporter is enabled)
logger.Info("Starting metrics server on %s", tcfg.AdminAddr) logger.Debug("Starting metrics server on %s", tcfg.AdminAddr)
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }) mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) })
if tel.PrometheusHandler != nil { if tel.PrometheusHandler != nil {
@@ -439,13 +447,6 @@ func runNewtMain(ctx context.Context) {
defer func() { _ = tel.Shutdown(context.Background()) }() defer func() { _ = tel.Shutdown(context.Background()) }()
} }
if *version {
fmt.Println("Newt version " + newtVersion)
os.Exit(0)
} else {
logger.Info("Newt version %s", newtVersion)
}
if err := updates.CheckForUpdate("fosrl", "newt", newtVersion); err != nil { if err := updates.CheckForUpdate("fosrl", "newt", newtVersion); err != nil {
logger.Error("Error checking for updates: %v\n", err) logger.Error("Error checking for updates: %v\n", err)
} }
@@ -691,7 +692,12 @@ func runNewtMain(ctx context.Context) {
return return
} }
clientsHandleNewtConnection(wgData.PublicKey, endpoint) relayPort := wgData.RelayPort
if relayPort == 0 {
relayPort = 21820
}
clientsHandleNewtConnection(wgData.PublicKey, endpoint, relayPort)
// Configure WireGuard // Configure WireGuard
config := fmt.Sprintf(`private_key=%s config := fmt.Sprintf(`private_key=%s

View File

@@ -10,12 +10,18 @@ import (
"fmt" "fmt"
"io" "io"
"net" "net"
"net/netip"
"os/exec"
"sync" "sync"
"time" "time"
"github.com/fosrl/newt/logger" "github.com/fosrl/newt/logger"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"gvisor.dev/gvisor/pkg/tcpip" "gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet" "gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
"gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/stack" "gvisor.dev/gvisor/pkg/tcpip/stack"
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp" "gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp" "gvisor.dev/gvisor/pkg/tcpip/transport/udp"
@@ -58,6 +64,9 @@ const (
// Buffer size for copying data // Buffer size for copying data
bufferSize = 32 * 1024 bufferSize = 32 * 1024
// icmpTimeout is the default timeout for ICMP ping requests.
icmpTimeout = 5 * time.Second
) )
// TCPHandler handles TCP connections from netstack // TCPHandler handles TCP connections from netstack
@@ -72,6 +81,12 @@ type UDPHandler struct {
proxyHandler *ProxyHandler proxyHandler *ProxyHandler
} }
// ICMPHandler handles ICMP packets from netstack
type ICMPHandler struct {
stack *stack.Stack
proxyHandler *ProxyHandler
}
// NewTCPHandler creates a new TCP handler // NewTCPHandler creates a new TCP handler
func NewTCPHandler(s *stack.Stack, ph *ProxyHandler) *TCPHandler { func NewTCPHandler(s *stack.Stack, ph *ProxyHandler) *TCPHandler {
return &TCPHandler{stack: s, proxyHandler: ph} return &TCPHandler{stack: s, proxyHandler: ph}
@@ -82,6 +97,11 @@ func NewUDPHandler(s *stack.Stack, ph *ProxyHandler) *UDPHandler {
return &UDPHandler{stack: s, proxyHandler: ph} return &UDPHandler{stack: s, proxyHandler: ph}
} }
// NewICMPHandler creates a new ICMP handler
func NewICMPHandler(s *stack.Stack, ph *ProxyHandler) *ICMPHandler {
return &ICMPHandler{stack: s, proxyHandler: ph}
}
// InstallTCPHandler installs the TCP forwarder on the stack // InstallTCPHandler installs the TCP forwarder on the stack
func (h *TCPHandler) InstallTCPHandler() error { func (h *TCPHandler) InstallTCPHandler() error {
tcpForwarder := tcp.NewForwarder(h.stack, defaultWndSize, maxConnAttempts, func(r *tcp.ForwarderRequest) { tcpForwarder := tcp.NewForwarder(h.stack, defaultWndSize, maxConnAttempts, func(r *tcp.ForwarderRequest) {
@@ -348,3 +368,334 @@ func copyPacketData(dst, src net.PacketConn, to net.Addr, timeout time.Duration)
dst.SetReadDeadline(time.Now().Add(timeout)) dst.SetReadDeadline(time.Now().Add(timeout))
} }
} }
// InstallICMPHandler installs the ICMP handler on the stack
func (h *ICMPHandler) InstallICMPHandler() error {
h.stack.SetTransportProtocolHandler(header.ICMPv4ProtocolNumber, h.handleICMPPacket)
logger.Debug("ICMP Handler: Installed ICMP protocol handler")
return nil
}
// handleICMPPacket handles incoming ICMP packets
func (h *ICMPHandler) handleICMPPacket(id stack.TransportEndpointID, pkt *stack.PacketBuffer) bool {
logger.Debug("ICMP Handler: Received ICMP packet from %s to %s", id.RemoteAddress, id.LocalAddress)
// Get the ICMP header from the packet
icmpData := pkt.TransportHeader().Slice()
if len(icmpData) < header.ICMPv4MinimumSize {
logger.Debug("ICMP Handler: Packet too small for ICMP header: %d bytes", len(icmpData))
return false
}
icmpHdr := header.ICMPv4(icmpData)
icmpType := icmpHdr.Type()
icmpCode := icmpHdr.Code()
logger.Debug("ICMP Handler: Type=%d, Code=%d, Ident=%d, Seq=%d",
icmpType, icmpCode, icmpHdr.Ident(), icmpHdr.Sequence())
// Only handle Echo Request (ping)
if icmpType != header.ICMPv4Echo {
logger.Debug("ICMP Handler: Ignoring non-echo ICMP type: %d", icmpType)
return false
}
// Extract source and destination addresses
srcIP := id.RemoteAddress.String()
dstIP := id.LocalAddress.String()
logger.Info("ICMP Handler: Echo Request from %s to %s (ident=%d, seq=%d)",
srcIP, dstIP, icmpHdr.Ident(), icmpHdr.Sequence())
// Convert to netip.Addr for subnet matching
srcAddr, err := netip.ParseAddr(srcIP)
if err != nil {
logger.Debug("ICMP Handler: Failed to parse source IP %s: %v", srcIP, err)
return false
}
dstAddr, err := netip.ParseAddr(dstIP)
if err != nil {
logger.Debug("ICMP Handler: Failed to parse dest IP %s: %v", dstIP, err)
return false
}
// Check subnet rules (use port 0 for ICMP since it doesn't have ports)
if h.proxyHandler == nil {
logger.Debug("ICMP Handler: No proxy handler configured")
return false
}
matchedRule := h.proxyHandler.subnetLookup.Match(srcAddr, dstAddr, 0, header.ICMPv4ProtocolNumber)
if matchedRule == nil {
logger.Debug("ICMP Handler: No matching subnet rule for %s -> %s", srcIP, dstIP)
return false
}
logger.Info("ICMP Handler: Matched subnet rule for %s -> %s", srcIP, dstIP)
// Determine actual destination (with possible rewrite)
actualDstIP := dstIP
if matchedRule.RewriteTo != "" {
resolvedAddr, err := h.proxyHandler.resolveRewriteAddress(matchedRule.RewriteTo)
if err != nil {
logger.Info("ICMP Handler: Failed to resolve rewrite address %s: %v", matchedRule.RewriteTo, err)
} else {
actualDstIP = resolvedAddr.String()
logger.Info("ICMP Handler: Using rewritten destination %s (original: %s)", actualDstIP, dstIP)
}
}
// Get the full ICMP payload (including the data after the header)
icmpPayload := pkt.Data().AsRange().ToSlice()
// Handle the ping in a goroutine to avoid blocking
go h.proxyPing(srcIP, dstIP, actualDstIP, icmpHdr.Ident(), icmpHdr.Sequence(), icmpPayload)
return true
}
// proxyPing sends a ping to the actual destination and injects the reply back
func (h *ICMPHandler) proxyPing(srcIP, originalDstIP, actualDstIP string, ident, seq uint16, payload []byte) {
logger.Debug("ICMP Handler: Proxying ping from %s to %s (actual: %s), ident=%d, seq=%d",
srcIP, originalDstIP, actualDstIP, ident, seq)
// Try three methods in order: ip4:icmp -> udp4 -> ping command
// Track which method succeeded so we can handle identifier matching correctly
method, success := h.tryICMPMethods(actualDstIP, ident, seq, payload)
if !success {
logger.Info("ICMP Handler: All ping methods failed for %s", actualDstIP)
return
}
logger.Info("ICMP Handler: Ping successful to %s using %s, injecting reply (ident=%d, seq=%d)",
actualDstIP, method, ident, seq)
// Build the reply packet to inject back into the netstack
// The reply should appear to come from the original destination (before rewrite)
h.injectICMPReply(srcIP, originalDstIP, ident, seq, payload)
}
// tryICMPMethods tries all available ICMP methods in order
func (h *ICMPHandler) tryICMPMethods(actualDstIP string, ident, seq uint16, payload []byte) (string, bool) {
if h.tryRawICMP(actualDstIP, ident, seq, payload, false) {
return "raw ICMP", true
}
if h.tryUnprivilegedICMP(actualDstIP, ident, seq, payload) {
return "unprivileged ICMP", true
}
if h.tryPingCommand(actualDstIP, ident, seq, payload) {
return "ping command", true
}
return "", false
}
// tryRawICMP attempts to ping using raw ICMP sockets (requires CAP_NET_RAW or root)
func (h *ICMPHandler) tryRawICMP(actualDstIP string, ident, seq uint16, payload []byte, ignoreIdent bool) bool {
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil {
logger.Debug("ICMP Handler: Raw ICMP socket not available: %v", err)
return false
}
defer conn.Close()
logger.Debug("ICMP Handler: Using raw ICMP socket")
return h.sendAndReceiveICMP(conn, actualDstIP, ident, seq, payload, false, ignoreIdent)
}
// tryUnprivilegedICMP attempts to ping using unprivileged ICMP (requires ping_group_range configured)
func (h *ICMPHandler) tryUnprivilegedICMP(actualDstIP string, ident, seq uint16, payload []byte) bool {
conn, err := icmp.ListenPacket("udp4", "0.0.0.0")
if err != nil {
logger.Debug("ICMP Handler: Unprivileged ICMP socket not available: %v", err)
return false
}
defer conn.Close()
logger.Debug("ICMP Handler: Using unprivileged ICMP socket")
// Unprivileged ICMP doesn't let us control the identifier, so we ignore it in matching
return h.sendAndReceiveICMP(conn, actualDstIP, ident, seq, payload, true, true)
}
// sendAndReceiveICMP sends an ICMP echo request and waits for the reply
func (h *ICMPHandler) sendAndReceiveICMP(conn *icmp.PacketConn, actualDstIP string, ident, seq uint16, payload []byte, isUnprivileged bool, ignoreIdent bool) bool {
// Build the ICMP echo request message
echoMsg := &icmp.Message{
Type: ipv4.ICMPTypeEcho,
Code: 0,
Body: &icmp.Echo{
ID: int(ident),
Seq: int(seq),
Data: payload,
},
}
msgBytes, err := echoMsg.Marshal(nil)
if err != nil {
logger.Debug("ICMP Handler: Failed to marshal ICMP message: %v", err)
return false
}
// Resolve destination address based on socket type
var writeErr error
if isUnprivileged {
// For unprivileged ICMP, use UDP-style addressing
udpAddr := &net.UDPAddr{IP: net.ParseIP(actualDstIP)}
logger.Debug("ICMP Handler: Sending ping to %s (unprivileged)", udpAddr.String())
conn.SetDeadline(time.Now().Add(icmpTimeout))
_, writeErr = conn.WriteTo(msgBytes, udpAddr)
} else {
// For raw ICMP, use IP addressing
dst, err := net.ResolveIPAddr("ip4", actualDstIP)
if err != nil {
logger.Debug("ICMP Handler: Failed to resolve destination %s: %v", actualDstIP, err)
return false
}
logger.Debug("ICMP Handler: Sending ping to %s (raw)", dst.String())
conn.SetDeadline(time.Now().Add(icmpTimeout))
_, writeErr = conn.WriteTo(msgBytes, dst)
}
if writeErr != nil {
logger.Debug("ICMP Handler: Failed to send ping to %s: %v", actualDstIP, writeErr)
return false
}
logger.Debug("ICMP Handler: Ping sent to %s, waiting for reply (ident=%d, seq=%d)", actualDstIP, ident, seq)
// Wait for reply - loop to filter out non-matching packets
replyBuf := make([]byte, 1500)
for {
n, peer, err := conn.ReadFrom(replyBuf)
if err != nil {
logger.Debug("ICMP Handler: Failed to receive ping reply from %s: %v", actualDstIP, err)
return false
}
logger.Debug("ICMP Handler: Received %d bytes from %s", n, peer.String())
// Parse the reply
replyMsg, err := icmp.ParseMessage(1, replyBuf[:n])
if err != nil {
logger.Debug("ICMP Handler: Failed to parse ICMP message: %v", err)
continue
}
// Check if it's an echo reply (type 0), not an echo request (type 8)
if replyMsg.Type != ipv4.ICMPTypeEchoReply {
logger.Debug("ICMP Handler: Received non-echo-reply type: %v, continuing to wait", replyMsg.Type)
continue
}
reply, ok := replyMsg.Body.(*icmp.Echo)
if !ok {
logger.Debug("ICMP Handler: Invalid echo reply body type, continuing to wait")
continue
}
// Verify the sequence matches what we sent
// For unprivileged ICMP, the kernel controls the identifier, so we only check sequence
if reply.Seq != int(seq) {
logger.Debug("ICMP Handler: Reply seq mismatch: got seq=%d, want seq=%d", reply.Seq, seq)
continue
}
if !ignoreIdent && reply.ID != int(ident) {
logger.Debug("ICMP Handler: Reply ident mismatch: got ident=%d, want ident=%d", reply.ID, ident)
continue
}
// Found matching reply
logger.Debug("ICMP Handler: Received valid echo reply")
return true
}
}
// tryPingCommand attempts to ping using the system ping command (always works, but less control)
func (h *ICMPHandler) tryPingCommand(actualDstIP string, ident, seq uint16, payload []byte) bool {
logger.Debug("ICMP Handler: Attempting to use system ping command")
ctx, cancel := context.WithTimeout(context.Background(), icmpTimeout)
defer cancel()
// Send one ping with timeout
// -c 1: count = 1 packet
// -W 5: timeout = 5 seconds
// -q: quiet output (just summary)
cmd := exec.CommandContext(ctx, "ping", "-c", "1", "-W", "5", "-q", actualDstIP)
output, err := cmd.CombinedOutput()
if err != nil {
logger.Debug("ICMP Handler: System ping command failed: %v, output: %s", err, string(output))
return false
}
logger.Debug("ICMP Handler: System ping command succeeded")
return true
}
// injectICMPReply creates an ICMP echo reply packet and queues it to be sent back through the tunnel
func (h *ICMPHandler) injectICMPReply(dstIP, srcIP string, ident, seq uint16, payload []byte) {
logger.Debug("ICMP Handler: Creating reply from %s to %s (ident=%d, seq=%d)",
srcIP, dstIP, ident, seq)
// Parse addresses
srcAddr, err := netip.ParseAddr(srcIP)
if err != nil {
logger.Info("ICMP Handler: Failed to parse source IP for reply: %v", err)
return
}
dstAddr, err := netip.ParseAddr(dstIP)
if err != nil {
logger.Info("ICMP Handler: Failed to parse dest IP for reply: %v", err)
return
}
// Calculate total packet size
ipHeaderLen := header.IPv4MinimumSize
icmpHeaderLen := header.ICMPv4MinimumSize
totalLen := ipHeaderLen + icmpHeaderLen + len(payload)
// Create the packet buffer
pkt := make([]byte, totalLen)
// Build IPv4 header
ipHdr := header.IPv4(pkt[:ipHeaderLen])
ipHdr.Encode(&header.IPv4Fields{
TotalLength: uint16(totalLen),
TTL: 64,
Protocol: uint8(header.ICMPv4ProtocolNumber),
SrcAddr: tcpip.AddrFrom4(srcAddr.As4()),
DstAddr: tcpip.AddrFrom4(dstAddr.As4()),
})
ipHdr.SetChecksum(^ipHdr.CalculateChecksum())
// Build ICMP header
icmpHdr := header.ICMPv4(pkt[ipHeaderLen : ipHeaderLen+icmpHeaderLen])
icmpHdr.SetType(header.ICMPv4EchoReply)
icmpHdr.SetCode(0)
icmpHdr.SetIdent(ident)
icmpHdr.SetSequence(seq)
// Copy payload
copy(pkt[ipHeaderLen+icmpHeaderLen:], payload)
// Calculate ICMP checksum (covers ICMP header + payload)
icmpHdr.SetChecksum(0)
icmpData := pkt[ipHeaderLen:]
icmpHdr.SetChecksum(^checksum.Checksum(icmpData, 0))
logger.Debug("ICMP Handler: Built reply packet, total length=%d", totalLen)
// Queue the packet to be sent back through the tunnel
if h.proxyHandler != nil {
if h.proxyHandler.QueueICMPReply(pkt) {
logger.Info("ICMP Handler: Queued echo reply packet for transmission")
} else {
logger.Info("ICMP Handler: Failed to queue echo reply packet")
}
} else {
logger.Info("ICMP Handler: Cannot queue reply - proxy handler not available")
}
}

View File

@@ -22,10 +22,12 @@ import (
"gvisor.dev/gvisor/pkg/tcpip/transport/udp" "gvisor.dev/gvisor/pkg/tcpip/transport/udp"
) )
// PortRange represents an allowed range of ports (inclusive) // PortRange represents an allowed range of ports (inclusive) with optional protocol filtering
// Protocol can be "tcp", "udp", or "" (empty string means both protocols)
type PortRange struct { type PortRange struct {
Min uint16 Min uint16
Max uint16 Max uint16
Protocol string // "tcp", "udp", or "" for both
} }
// SubnetRule represents a subnet with optional port restrictions and source address // SubnetRule represents a subnet with optional port restrictions and source address
@@ -41,6 +43,7 @@ type PortRange struct {
type SubnetRule struct { type SubnetRule struct {
SourcePrefix netip.Prefix // Source IP prefix (who is sending) SourcePrefix netip.Prefix // Source IP prefix (who is sending)
DestPrefix netip.Prefix // Destination IP prefix (where it's going) DestPrefix netip.Prefix // Destination IP prefix (where it's going)
DisableIcmp bool // If true, ICMP traffic is blocked for this subnet
RewriteTo string // Optional rewrite address for DNAT - can be IP/CIDR or domain name RewriteTo string // Optional rewrite address for DNAT - can be IP/CIDR or domain name
PortRanges []PortRange // empty slice means all ports allowed PortRanges []PortRange // empty slice means all ports allowed
} }
@@ -67,7 +70,7 @@ func NewSubnetLookup() *SubnetLookup {
// AddSubnet adds a subnet rule with source and destination prefixes and optional port restrictions // AddSubnet adds a subnet rule with source and destination prefixes and optional port restrictions
// If portRanges is nil or empty, all ports are allowed for this subnet // If portRanges is nil or empty, all ports are allowed for this subnet
// rewriteTo can be either an IP/CIDR (e.g., "192.168.1.1/32") or a domain name (e.g., "example.com") // rewriteTo can be either an IP/CIDR (e.g., "192.168.1.1/32") or a domain name (e.g., "example.com")
func (sl *SubnetLookup) AddSubnet(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange) { func (sl *SubnetLookup) AddSubnet(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange, disableIcmp bool) {
sl.mu.Lock() sl.mu.Lock()
defer sl.mu.Unlock() defer sl.mu.Unlock()
@@ -79,6 +82,7 @@ func (sl *SubnetLookup) AddSubnet(sourcePrefix, destPrefix netip.Prefix, rewrite
sl.rules[key] = &SubnetRule{ sl.rules[key] = &SubnetRule{
SourcePrefix: sourcePrefix, SourcePrefix: sourcePrefix,
DestPrefix: destPrefix, DestPrefix: destPrefix,
DisableIcmp: disableIcmp,
RewriteTo: rewriteTo, RewriteTo: rewriteTo,
PortRanges: portRanges, PortRanges: portRanges,
} }
@@ -97,14 +101,16 @@ func (sl *SubnetLookup) RemoveSubnet(sourcePrefix, destPrefix netip.Prefix) {
delete(sl.rules, key) delete(sl.rules, key)
} }
// Match checks if a source IP, destination IP, and port match any subnet rule // Match checks if a source IP, destination IP, port, and protocol match any subnet rule
// Returns the matched rule if BOTH: // Returns the matched rule if ALL of these conditions are met:
// - The source IP is in the rule's source prefix // - The source IP is in the rule's source prefix
// - The destination IP is in the rule's destination prefix // - The destination IP is in the rule's destination prefix
// - The port is in an allowed range (or no port restrictions exist) // - The port is in an allowed range (or no port restrictions exist)
// - The protocol matches (or the port range allows both protocols)
// //
// proto should be header.TCPProtocolNumber or header.UDPProtocolNumber
// Returns nil if no rule matches // Returns nil if no rule matches
func (sl *SubnetLookup) Match(srcIP, dstIP netip.Addr, port uint16) *SubnetRule { func (sl *SubnetLookup) Match(srcIP, dstIP netip.Addr, port uint16, proto tcpip.TransportProtocolNumber) *SubnetRule {
sl.mu.RLock() sl.mu.RLock()
defer sl.mu.RUnlock() defer sl.mu.RUnlock()
@@ -119,17 +125,32 @@ func (sl *SubnetLookup) Match(srcIP, dstIP netip.Addr, port uint16) *SubnetRule
continue continue
} }
if rule.DisableIcmp && (proto == header.ICMPv4ProtocolNumber || proto == header.ICMPv6ProtocolNumber) {
// ICMP is disabled for this subnet
return nil
}
// Both IPs match - now check port restrictions // Both IPs match - now check port restrictions
// If no port ranges specified, all ports are allowed // If no port ranges specified, all ports are allowed
if len(rule.PortRanges) == 0 { if len(rule.PortRanges) == 0 {
return rule return rule
} }
// Check if port is in any of the allowed ranges // Check if port and protocol are in any of the allowed ranges
for _, pr := range rule.PortRanges { for _, pr := range rule.PortRanges {
if port >= pr.Min && port <= pr.Max { if port >= pr.Min && port <= pr.Max {
// Check protocol compatibility
if pr.Protocol == "" {
// Empty protocol means allow both TCP and UDP
return rule return rule
} }
// Check if the packet protocol matches the port range protocol
if (pr.Protocol == "tcp" && proto == header.TCPProtocolNumber) ||
(pr.Protocol == "udp" && proto == header.UDPProtocolNumber) {
return rule
}
// Port matches but protocol doesn't - continue checking other ranges
}
} }
} }
@@ -166,23 +187,27 @@ type ProxyHandler struct {
proxyNotifyHandle *channel.NotificationHandle proxyNotifyHandle *channel.NotificationHandle
tcpHandler *TCPHandler tcpHandler *TCPHandler
udpHandler *UDPHandler udpHandler *UDPHandler
icmpHandler *ICMPHandler
subnetLookup *SubnetLookup subnetLookup *SubnetLookup
natTable map[connKey]*natState natTable map[connKey]*natState
destRewriteTable map[destKey]netip.Addr // Maps original dest to rewritten dest for handler lookups destRewriteTable map[destKey]netip.Addr // Maps original dest to rewritten dest for handler lookups
natMu sync.RWMutex natMu sync.RWMutex
enabled bool enabled bool
icmpReplies chan []byte // Channel for ICMP reply packets to be sent back through the tunnel
notifiable channel.Notification // Notification handler for triggering reads
} }
// ProxyHandlerOptions configures the proxy handler // ProxyHandlerOptions configures the proxy handler
type ProxyHandlerOptions struct { type ProxyHandlerOptions struct {
EnableTCP bool EnableTCP bool
EnableUDP bool EnableUDP bool
EnableICMP bool
MTU int MTU int
} }
// NewProxyHandler creates a new proxy handler for promiscuous mode // NewProxyHandler creates a new proxy handler for promiscuous mode
func NewProxyHandler(options ProxyHandlerOptions) (*ProxyHandler, error) { func NewProxyHandler(options ProxyHandlerOptions) (*ProxyHandler, error) {
if !options.EnableTCP && !options.EnableUDP { if !options.EnableTCP && !options.EnableUDP && !options.EnableICMP {
return nil, nil // No proxy needed return nil, nil // No proxy needed
} }
@@ -191,6 +216,7 @@ func NewProxyHandler(options ProxyHandlerOptions) (*ProxyHandler, error) {
subnetLookup: NewSubnetLookup(), subnetLookup: NewSubnetLookup(),
natTable: make(map[connKey]*natState), natTable: make(map[connKey]*natState),
destRewriteTable: make(map[destKey]netip.Addr), destRewriteTable: make(map[destKey]netip.Addr),
icmpReplies: make(chan []byte, 256), // Buffer for ICMP reply packets
proxyEp: channel.New(1024, uint32(options.MTU), ""), proxyEp: channel.New(1024, uint32(options.MTU), ""),
proxyStack: stack.New(stack.Options{ proxyStack: stack.New(stack.Options{
NetworkProtocols: []stack.NetworkProtocolFactory{ NetworkProtocols: []stack.NetworkProtocolFactory{
@@ -222,6 +248,15 @@ func NewProxyHandler(options ProxyHandlerOptions) (*ProxyHandler, error) {
} }
} }
// Initialize ICMP handler if enabled
if options.EnableICMP {
handler.icmpHandler = NewICMPHandler(handler.proxyStack, handler)
if err := handler.icmpHandler.InstallICMPHandler(); err != nil {
return nil, fmt.Errorf("failed to install ICMP handler: %v", err)
}
logger.Debug("ProxyHandler: ICMP handler enabled")
}
// // Example 1: Add a rule with no port restrictions (all ports allowed) // // Example 1: Add a rule with no port restrictions (all ports allowed)
// // This accepts all traffic FROM 10.0.0.0/24 TO 10.20.20.0/24 // // This accepts all traffic FROM 10.0.0.0/24 TO 10.20.20.0/24
// sourceSubnet := netip.MustParsePrefix("10.0.0.0/24") // sourceSubnet := netip.MustParsePrefix("10.0.0.0/24")
@@ -246,11 +281,11 @@ func NewProxyHandler(options ProxyHandlerOptions) (*ProxyHandler, error) {
// destPrefix: The IP prefix of the destination // destPrefix: The IP prefix of the destination
// rewriteTo: Optional address to rewrite destination to - can be IP/CIDR or domain name // rewriteTo: Optional address to rewrite destination to - can be IP/CIDR or domain name
// If portRanges is nil or empty, all ports are allowed for this subnet // If portRanges is nil or empty, all ports are allowed for this subnet
func (p *ProxyHandler) AddSubnetRule(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange) { func (p *ProxyHandler) AddSubnetRule(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange, disableIcmp bool) {
if p == nil || !p.enabled { if p == nil || !p.enabled {
return return
} }
p.subnetLookup.AddSubnet(sourcePrefix, destPrefix, rewriteTo, portRanges) p.subnetLookup.AddSubnet(sourcePrefix, destPrefix, rewriteTo, portRanges, disableIcmp)
} }
// RemoveSubnetRule removes a subnet from the proxy handler // RemoveSubnetRule removes a subnet from the proxy handler
@@ -329,6 +364,9 @@ func (p *ProxyHandler) Initialize(notifiable channel.Notification) error {
return nil return nil
} }
// Store notifiable for triggering notifications on ICMP replies
p.notifiable = notifiable
// Add notification handler // Add notification handler
p.proxyNotifyHandle = p.proxyEp.AddNotify(notifiable) p.proxyNotifyHandle = p.proxyEp.AddNotify(notifiable)
@@ -407,14 +445,21 @@ func (p *ProxyHandler) HandleIncomingPacket(packet []byte) bool {
} }
udpHeader := header.UDP(packet[headerLen:]) udpHeader := header.UDP(packet[headerLen:])
dstPort = udpHeader.DestinationPort() dstPort = udpHeader.DestinationPort()
default: case header.ICMPv4ProtocolNumber:
// For other protocols (ICMP, etc.), use port 0 (must match rules with no port restrictions) // ICMP doesn't have ports, use port 0 (must match rules with no port restrictions)
dstPort = 0 dstPort = 0
logger.Debug("HandleIncomingPacket: ICMP packet from %s to %s", srcAddr, dstAddr)
default:
// For other protocols, use port 0 (must match rules with no port restrictions)
dstPort = 0
logger.Debug("HandleIncomingPacket: Unknown protocol %d from %s to %s", protocol, srcAddr, dstAddr)
} }
// Check if the source IP, destination IP, and port match any subnet rule // Check if the source IP, destination IP, port, and protocol match any subnet rule
matchedRule := p.subnetLookup.Match(srcAddr, dstAddr, dstPort) matchedRule := p.subnetLookup.Match(srcAddr, dstAddr, dstPort, protocol)
if matchedRule != nil { if matchedRule != nil {
logger.Debug("HandleIncomingPacket: Matched rule for %s -> %s (proto=%d, port=%d)",
srcAddr, dstAddr, protocol, dstPort)
// Check if we need to perform DNAT // Check if we need to perform DNAT
if matchedRule.RewriteTo != "" { if matchedRule.RewriteTo != "" {
// Create connection tracking key using original destination // Create connection tracking key using original destination
@@ -501,9 +546,12 @@ func (p *ProxyHandler) HandleIncomingPacket(packet []byte) bool {
Payload: buffer.MakeWithData(packet), Payload: buffer.MakeWithData(packet),
}) })
p.proxyEp.InjectInbound(header.IPv4ProtocolNumber, pkb) p.proxyEp.InjectInbound(header.IPv4ProtocolNumber, pkb)
logger.Debug("HandleIncomingPacket: Injected packet into proxy stack (proto=%d)", protocol)
return true return true
} }
logger.Debug("HandleIncomingPacket: No matching rule for %s -> %s (proto=%d, port=%d)",
srcAddr, dstAddr, protocol, dstPort)
return false return false
} }
@@ -626,6 +674,15 @@ func (p *ProxyHandler) ReadOutgoingPacket() *buffer.View {
return nil return nil
} }
// First check for ICMP reply packets (non-blocking)
select {
case icmpReply := <-p.icmpReplies:
logger.Debug("ReadOutgoingPacket: Returning ICMP reply packet (%d bytes)", len(icmpReply))
return buffer.NewViewWithData(icmpReply)
default:
// No ICMP reply available, continue to check proxy endpoint
}
pkt := p.proxyEp.Read() pkt := p.proxyEp.Read()
if pkt != nil { if pkt != nil {
view := pkt.ToView() view := pkt.ToView()
@@ -655,6 +712,11 @@ func (p *ProxyHandler) ReadOutgoingPacket() *buffer.View {
srcPort = udpHeader.SourcePort() srcPort = udpHeader.SourcePort()
dstPort = udpHeader.DestinationPort() dstPort = udpHeader.DestinationPort()
} }
case header.ICMPv4ProtocolNumber:
// ICMP packets don't need NAT translation in our implementation
// since we construct reply packets with the correct addresses
logger.Debug("ReadOutgoingPacket: ICMP packet from %s to %s", srcIP, dstIP)
return view
} }
// Look up NAT state for reverse translation // Look up NAT state for reverse translation
@@ -688,12 +750,37 @@ func (p *ProxyHandler) ReadOutgoingPacket() *buffer.View {
return nil return nil
} }
// QueueICMPReply queues an ICMP reply packet to be sent back through the tunnel
func (p *ProxyHandler) QueueICMPReply(packet []byte) bool {
if p == nil || !p.enabled {
return false
}
select {
case p.icmpReplies <- packet:
logger.Debug("QueueICMPReply: Queued ICMP reply packet (%d bytes)", len(packet))
// Trigger notification so WriteNotify picks up the packet
if p.notifiable != nil {
p.notifiable.WriteNotify()
}
return true
default:
logger.Info("QueueICMPReply: ICMP reply channel full, dropping packet")
return false
}
}
// Close cleans up the proxy handler resources // Close cleans up the proxy handler resources
func (p *ProxyHandler) Close() error { func (p *ProxyHandler) Close() error {
if p == nil || !p.enabled { if p == nil || !p.enabled {
return nil return nil
} }
// Close ICMP replies channel
if p.icmpReplies != nil {
close(p.icmpReplies)
}
if p.proxyStack != nil { if p.proxyStack != nil {
p.proxyStack.RemoveNIC(1) p.proxyStack.RemoveNIC(1)
p.proxyStack.Close() p.proxyStack.Close()

View File

@@ -58,6 +58,7 @@ type Net netTun
type NetTunOptions struct { type NetTunOptions struct {
EnableTCPProxy bool EnableTCPProxy bool
EnableUDPProxy bool EnableUDPProxy bool
EnableICMPProxy bool
} }
// CreateNetTUN creates a new TUN device with netstack without proxying // CreateNetTUN creates a new TUN device with netstack without proxying
@@ -65,6 +66,7 @@ func CreateNetTUN(localAddresses, dnsServers []netip.Addr, mtu int) (tun.Device,
return CreateNetTUNWithOptions(localAddresses, dnsServers, mtu, NetTunOptions{ return CreateNetTUNWithOptions(localAddresses, dnsServers, mtu, NetTunOptions{
EnableTCPProxy: true, EnableTCPProxy: true,
EnableUDPProxy: true, EnableUDPProxy: true,
EnableICMPProxy: true,
}) })
} }
@@ -84,12 +86,13 @@ func CreateNetTUNWithOptions(localAddresses, dnsServers []netip.Addr, mtu int, o
mtu: mtu, mtu: mtu,
} }
// Initialize proxy handler if TCP or UDP proxying is enabled // Initialize proxy handler if TCP, UDP, or ICMP proxying is enabled
if options.EnableTCPProxy || options.EnableUDPProxy { if options.EnableTCPProxy || options.EnableUDPProxy || options.EnableICMPProxy {
var err error var err error
dev.proxyHandler, err = NewProxyHandler(ProxyHandlerOptions{ dev.proxyHandler, err = NewProxyHandler(ProxyHandlerOptions{
EnableTCP: options.EnableTCPProxy, EnableTCP: options.EnableTCPProxy,
EnableUDP: options.EnableUDPProxy, EnableUDP: options.EnableUDPProxy,
EnableICMP: options.EnableICMPProxy,
MTU: mtu, MTU: mtu,
}) })
if err != nil { if err != nil {
@@ -351,10 +354,10 @@ func (net *Net) ListenUDP(laddr *net.UDPAddr) (*gonet.UDPConn, error) {
// AddProxySubnetRule adds a subnet rule to the proxy handler // AddProxySubnetRule adds a subnet rule to the proxy handler
// If portRanges is nil or empty, all ports are allowed for this subnet // If portRanges is nil or empty, all ports are allowed for this subnet
// rewriteTo can be either an IP/CIDR (e.g., "192.168.1.1/32") or a domain name (e.g., "example.com") // rewriteTo can be either an IP/CIDR (e.g., "192.168.1.1/32") or a domain name (e.g., "example.com")
func (net *Net) AddProxySubnetRule(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange) { func (net *Net) AddProxySubnetRule(sourcePrefix, destPrefix netip.Prefix, rewriteTo string, portRanges []PortRange, disableIcmp bool) {
tun := (*netTun)(net) tun := (*netTun)(net)
if tun.proxyHandler != nil { if tun.proxyHandler != nil {
tun.proxyHandler.AddSubnetRule(sourcePrefix, destPrefix, rewriteTo, portRanges) tun.proxyHandler.AddSubnetRule(sourcePrefix, destPrefix, rewriteTo, portRanges, disableIcmp)
} }
} }

49
udp_client.py Normal file
View File

@@ -0,0 +1,49 @@
import socket
import sys
# Argument parsing: Check if IP and Port are provided
if len(sys.argv) != 3:
print("Usage: python udp_client.py <HOST_IP> <HOST_PORT>")
# Example: python udp_client.py 127.0.0.1 12000
sys.exit(1)
HOST = sys.argv[1]
try:
PORT = int(sys.argv[2])
except ValueError:
print("Error: HOST_PORT must be an integer.")
sys.exit(1)
# The message to send to the server
MESSAGE = "Hello UDP Server! How are you?"
# Create a UDP socket
try:
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
except socket.error as err:
print(f"Failed to create socket: {err}")
sys.exit()
try:
print(f"Sending message to {HOST}:{PORT}...")
# Send the message (data must be encoded to bytes)
client_socket.sendto(MESSAGE.encode('utf-8'), (HOST, PORT))
# Wait for the server's response (buffer size 1024 bytes)
data, server_address = client_socket.recvfrom(1024)
# Decode and print the server's response
response = data.decode('utf-8')
print("-" * 30)
print(f"Received response from server {server_address[0]}:{server_address[1]}:")
print(f"-> Data: '{response}'")
except socket.error as err:
print(f"Error during communication: {err}")
finally:
# Close the socket
client_socket.close()
print("-" * 30)
print("Client finished and socket closed.")

58
udp_server.py Normal file
View File

@@ -0,0 +1,58 @@
import socket
import sys
# optionally take in some positional args for the port
if len(sys.argv) > 1:
try:
PORT = int(sys.argv[1])
except ValueError:
print("Invalid port number. Using default port 12000.")
PORT = 12000
else:
PORT = 12000
# Define the server host and port
HOST = '0.0.0.0' # Standard loopback interface address (localhost)
# Create a UDP socket
try:
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
except socket.error as err:
print(f"Failed to create socket: {err}")
sys.exit()
# Bind the socket to the address
try:
server_socket.bind((HOST, PORT))
print(f"UDP Server listening on {HOST}:{PORT}")
except socket.error as err:
print(f"Bind failed: {err}")
server_socket.close()
sys.exit()
# Wait for and process incoming data
while True:
try:
# Receive data and the client's address (buffer size 1024 bytes)
data, client_address = server_socket.recvfrom(1024)
# Decode the data and print the message
message = data.decode('utf-8')
print("-" * 30)
print(f"Received message from {client_address[0]}:{client_address[1]}:")
print(f"-> Data: '{message}'")
# Prepare the response message
response_message = f"Hello client! Server received: '{message.upper()}'"
# Send the response back to the client
server_socket.sendto(response_message.encode('utf-8'), client_address)
print(f"Sent response back to client.")
except Exception as e:
print(f"An error occurred: {e}")
break
# Clean up (though usually unreachable in an infinite server loop)
server_socket.close()
print("Server stopped.")

View File

@@ -38,7 +38,6 @@ type Server struct {
isRunning bool isRunning bool
runningLock sync.Mutex runningLock sync.Mutex
newtID string newtID string
outputPrefix string
useNetstack bool useNetstack bool
tnet interface{} // Will be *netstack2.Net when using netstack tnet interface{} // Will be *netstack2.Net when using netstack
} }
@@ -50,7 +49,6 @@ func NewServer(serverAddr string, serverPort uint16, newtID string) *Server {
serverPort: serverPort + 1, // use the next port for the server serverPort: serverPort + 1, // use the next port for the server
shutdownCh: make(chan struct{}), shutdownCh: make(chan struct{}),
newtID: newtID, newtID: newtID,
outputPrefix: "[WGTester] ",
useNetstack: false, useNetstack: false,
tnet: nil, tnet: nil,
} }
@@ -63,7 +61,6 @@ func NewServerWithNetstack(serverAddr string, serverPort uint16, newtID string,
serverPort: serverPort + 1, // use the next port for the server serverPort: serverPort + 1, // use the next port for the server
shutdownCh: make(chan struct{}), shutdownCh: make(chan struct{}),
newtID: newtID, newtID: newtID,
outputPrefix: "[WGTester] ",
useNetstack: true, useNetstack: true,
tnet: tnet, tnet: tnet,
} }
@@ -109,7 +106,7 @@ func (s *Server) Start() error {
s.isRunning = true s.isRunning = true
go s.handleConnections() go s.handleConnections()
logger.Info("%sServer started on %s:%d", s.outputPrefix, s.serverAddr, s.serverPort) logger.Debug("WGTester Server started on %s:%d", s.serverAddr, s.serverPort)
return nil return nil
} }
@@ -127,7 +124,7 @@ func (s *Server) Stop() {
s.conn.Close() s.conn.Close()
} }
s.isRunning = false s.isRunning = false
logger.Info("%sServer stopped", s.outputPrefix) logger.Info("WGTester Server stopped")
} }
// RestartWithNetstack stops the current server and restarts it with netstack // RestartWithNetstack stops the current server and restarts it with netstack
@@ -162,7 +159,7 @@ func (s *Server) handleConnections() {
// Set read deadline to avoid blocking forever // Set read deadline to avoid blocking forever
err := s.conn.SetReadDeadline(time.Now().Add(1 * time.Second)) err := s.conn.SetReadDeadline(time.Now().Add(1 * time.Second))
if err != nil { if err != nil {
logger.Error("%sError setting read deadline: %v", s.outputPrefix, err) logger.Error("Error setting read deadline: %v", err)
continue continue
} }
@@ -192,7 +189,7 @@ func (s *Server) handleConnections() {
if err == io.EOF { if err == io.EOF {
return return
} }
logger.Error("%sError reading from UDP: %v", s.outputPrefix, err) logger.Error("Error reading from UDP: %v", err)
} }
continue continue
} }
@@ -224,7 +221,7 @@ func (s *Server) handleConnections() {
copy(responsePacket[5:13], buffer[5:13]) copy(responsePacket[5:13], buffer[5:13])
// Log response being sent for debugging // Log response being sent for debugging
// logger.Debug("%sSending response to %s", s.outputPrefix, addr.String()) // logger.Debug("Sending response to %s", addr.String())
// Send the response packet - handle both regular UDP and netstack UDP // Send the response packet - handle both regular UDP and netstack UDP
if s.useNetstack { if s.useNetstack {
@@ -238,9 +235,9 @@ func (s *Server) handleConnections() {
} }
if err != nil { if err != nil {
logger.Error("%sError sending response: %v", s.outputPrefix, err) logger.Error("Error sending response: %v", err)
} else { } else {
// logger.Debug("%sResponse sent successfully", s.outputPrefix) // logger.Debug("Response sent successfully")
} }
} }
} }