mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-05 08:36:37 +00:00
Compare commits
42 Commits
enable-udp
...
v0.36.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d48edb9837 | ||
|
|
b41de7fcd1 | ||
|
|
18f84f0df5 | ||
|
|
44407a158a | ||
|
|
488b697479 | ||
|
|
5953b43ead | ||
|
|
58b2eb4b92 | ||
|
|
05415f72ec | ||
|
|
b7af53ea40 | ||
|
|
cee4aeea9e | ||
|
|
ca9aca9b19 | ||
|
|
e00a280329 | ||
|
|
fe370e7d8f | ||
|
|
125b5e2b16 | ||
|
|
97d498c59c | ||
|
|
0125cd97d8 | ||
|
|
7d385b8dc3 | ||
|
|
f930ef2ee6 | ||
|
|
771c99a523 | ||
|
|
e20be2397c | ||
|
|
46766e7e24 | ||
|
|
a7ddb8f1f8 | ||
|
|
7335c82553 | ||
|
|
a32ec97911 | ||
|
|
5c05131a94 | ||
|
|
b6abd4b4da | ||
|
|
2605948e01 | ||
|
|
eb2ac039c7 | ||
|
|
790a9ed7df | ||
|
|
2e61ce006d | ||
|
|
3cc485759e | ||
|
|
aafa9c67fc | ||
|
|
69f48db0a3 | ||
|
|
8c965434ae | ||
|
|
78da6b42ad | ||
|
|
1ad2cb5582 | ||
|
|
c619bf5b0c | ||
|
|
9f4db0a953 | ||
|
|
3e836db1d1 | ||
|
|
c01874e9ce | ||
|
|
1b2517ea20 | ||
|
|
3e9f0d57ac |
4
.github/workflows/golang-test-freebsd.yml
vendored
4
.github/workflows/golang-test-freebsd.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
copyback: false
|
copyback: false
|
||||||
release: "14.1"
|
release: "14.1"
|
||||||
prepare: |
|
prepare: |
|
||||||
pkg install -y go
|
pkg install -y go pkgconf xorg
|
||||||
|
|
||||||
# -x - to print all executed commands
|
# -x - to print all executed commands
|
||||||
# -e - to faile on first error
|
# -e - to faile on first error
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
time go build -o netbird client/main.go
|
time go build -o netbird client/main.go
|
||||||
# check all component except management, since we do not support management server on freebsd
|
# check all component except management, since we do not support management server on freebsd
|
||||||
time go test -timeout 1m -failfast ./base62/...
|
time go test -timeout 1m -failfast ./base62/...
|
||||||
# NOTE: without -p1 `client/internal/dns` will fail becasue of `listen udp4 :33100: bind: address already in use`
|
# NOTE: without -p1 `client/internal/dns` will fail because of `listen udp4 :33100: bind: address already in use`
|
||||||
time go test -timeout 8m -failfast -p 1 ./client/...
|
time go test -timeout 8m -failfast -p 1 ./client/...
|
||||||
time go test -timeout 1m -failfast ./dns/...
|
time go test -timeout 1m -failfast ./dns/...
|
||||||
time go test -timeout 1m -failfast ./encryption/...
|
time go test -timeout 1m -failfast ./encryption/...
|
||||||
|
|||||||
6
.github/workflows/golang-test-linux.yml
vendored
6
.github/workflows/golang-test-linux.yml
vendored
@@ -262,7 +262,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: [ '386','amd64' ]
|
arch: [ '386','amd64' ]
|
||||||
store: [ 'sqlite', 'postgres', 'mysql' ]
|
store: [ 'sqlite', 'postgres' ]
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
@@ -314,7 +314,7 @@ jobs:
|
|||||||
run: docker pull mlsmaycon/warmed-mysql:8
|
run: docker pull mlsmaycon/warmed-mysql:8
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=benchmark -run=^$ -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m $(go list ./... | grep /management)
|
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -run=^$ -tags=benchmark -bench=. -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 30m $(go list -tags=benchmark ./... | grep /management)
|
||||||
|
|
||||||
api_integration_test:
|
api_integration_test:
|
||||||
needs: [ build-cache ]
|
needs: [ build-cache ]
|
||||||
@@ -363,7 +363,7 @@ jobs:
|
|||||||
run: git --no-pager diff --exit-code
|
run: git --no-pager diff --exit-code
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 10m -tags=integration $(go list ./... | grep /management)
|
run: CGO_ENABLED=1 GOARCH=${{ matrix.arch }} NETBIRD_STORE_ENGINE=${{ matrix.store }} CI=true go test -tags=integration -p 1 -exec 'sudo --preserve-env=CI,NETBIRD_STORE_ENGINE' -timeout 30m $(go list -tags=integration ./... | grep /management)
|
||||||
|
|
||||||
test_client_on_docker:
|
test_client_on_docker:
|
||||||
needs: [ build-cache ]
|
needs: [ build-cache ]
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -9,10 +9,10 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SIGN_PIPE_VER: "v0.0.17"
|
SIGN_PIPE_VER: "v0.0.18"
|
||||||
GORELEASER_VER: "v2.3.2"
|
GORELEASER_VER: "v2.3.2"
|
||||||
PRODUCT_NAME: "NetBird"
|
PRODUCT_NAME: "NetBird"
|
||||||
COPYRIGHT: "Wiretrustee UG (haftungsbeschreankt)"
|
COPYRIGHT: "NetBird GmbH"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||||
|
|||||||
2
AUTHORS
2
AUTHORS
@@ -1,3 +1,3 @@
|
|||||||
Mikhail Bragin (https://github.com/braginini)
|
Mikhail Bragin (https://github.com/braginini)
|
||||||
Maycon Santos (https://github.com/mlsmaycon)
|
Maycon Santos (https://github.com/mlsmaycon)
|
||||||
Wiretrustee UG (haftungsbeschränkt)
|
NetBird GmbH
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
We are incredibly thankful for the contributions we receive from the community.
|
We are incredibly thankful for the contributions we receive from the community.
|
||||||
We require our external contributors to sign a Contributor License Agreement ("CLA") in
|
We require our external contributors to sign a Contributor License Agreement ("CLA") in
|
||||||
order to ensure that our projects remain licensed under Free and Open Source licenses such
|
order to ensure that our projects remain licensed under Free and Open Source licenses such
|
||||||
as BSD-3 while allowing Wiretrustee to build a sustainable business.
|
as BSD-3 while allowing NetBird to build a sustainable business.
|
||||||
|
|
||||||
Wiretrustee is committed to having a true Open Source Software ("OSS") license for
|
NetBird is committed to having a true Open Source Software ("OSS") license for
|
||||||
our software. A CLA enables Wiretrustee to safely commercialize our products
|
our software. A CLA enables NetBird to safely commercialize our products
|
||||||
while keeping a standard OSS license with all the rights that license grants to users: the
|
while keeping a standard OSS license with all the rights that license grants to users: the
|
||||||
ability to use the project in their own projects or businesses, to republish modified
|
ability to use the project in their own projects or businesses, to republish modified
|
||||||
source, or to completely fork the project.
|
source, or to completely fork the project.
|
||||||
@@ -20,11 +20,11 @@ This is a human-readable summary of (and not a substitute for) the full agreemen
|
|||||||
This highlights only some of key terms of the CLA. It has no legal value and you should
|
This highlights only some of key terms of the CLA. It has no legal value and you should
|
||||||
carefully review all the terms of the actual CLA before agreeing.
|
carefully review all the terms of the actual CLA before agreeing.
|
||||||
|
|
||||||
<li>Grant of copyright license. You give Wiretrustee permission to use your copyrighted work
|
<li>Grant of copyright license. You give NetBird permission to use your copyrighted work
|
||||||
in commercial products.
|
in commercial products.
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>Grant of patent license. If your contributed work uses a patent, you give Wiretrustee a
|
<li>Grant of patent license. If your contributed work uses a patent, you give NetBird a
|
||||||
license to use that patent including within commercial products. You also agree that you
|
license to use that patent including within commercial products. You also agree that you
|
||||||
have permission to grant this license.
|
have permission to grant this license.
|
||||||
</li>
|
</li>
|
||||||
@@ -45,7 +45,7 @@ more.
|
|||||||
# Why require a CLA?
|
# Why require a CLA?
|
||||||
|
|
||||||
Agreeing to a CLA explicitly states that you are entitled to provide a contribution, that you cannot withdraw permission
|
Agreeing to a CLA explicitly states that you are entitled to provide a contribution, that you cannot withdraw permission
|
||||||
to use your contribution at a later date, and that Wiretrustee has permission to use your contribution in our commercial
|
to use your contribution at a later date, and that NetBird has permission to use your contribution in our commercial
|
||||||
products.
|
products.
|
||||||
|
|
||||||
This removes any ambiguities or uncertainties caused by not having a CLA and allows users and customers to confidently
|
This removes any ambiguities or uncertainties caused by not having a CLA and allows users and customers to confidently
|
||||||
@@ -65,25 +65,25 @@ Follow the steps given by the bot to sign the CLA. This will require you to log
|
|||||||
information from your account) and to fill in a few additional details such as your name and email address. We will only
|
information from your account) and to fill in a few additional details such as your name and email address. We will only
|
||||||
use this information for CLA tracking; none of your submitted information will be used for marketing purposes.
|
use this information for CLA tracking; none of your submitted information will be used for marketing purposes.
|
||||||
|
|
||||||
You only have to sign the CLA once. Once you've signed the CLA, future contributions to any Wiretrustee project will not
|
You only have to sign the CLA once. Once you've signed the CLA, future contributions to any NetBird project will not
|
||||||
require you to sign again.
|
require you to sign again.
|
||||||
|
|
||||||
# Legal Terms and Agreement
|
# Legal Terms and Agreement
|
||||||
|
|
||||||
In order to clarify the intellectual property license granted with Contributions from any person or entity, Wiretrustee
|
In order to clarify the intellectual property license granted with Contributions from any person or entity, NetBird
|
||||||
UG (haftungsbeschränkt) ("Wiretrustee") must have a Contributor License Agreement ("CLA") on file that has been signed
|
GmbH ("NetBird") must have a Contributor License Agreement ("CLA") on file that has been signed
|
||||||
by each Contributor, indicating agreement to the license terms below. This license does not change your rights to use
|
by each Contributor, indicating agreement to the license terms below. This license does not change your rights to use
|
||||||
your own Contributions for any other purpose.
|
your own Contributions for any other purpose.
|
||||||
|
|
||||||
You accept and agree to the following terms and conditions for Your present and future Contributions submitted to
|
You accept and agree to the following terms and conditions for Your present and future Contributions submitted to
|
||||||
Wiretrustee. Except for the license granted herein to Wiretrustee and recipients of software distributed by Wiretrustee,
|
NetBird. Except for the license granted herein to NetBird and recipients of software distributed by NetBird,
|
||||||
You reserve all right, title, and interest in and to Your Contributions.
|
You reserve all right, title, and interest in and to Your Contributions.
|
||||||
|
|
||||||
1. Definitions.
|
1. Definitions.
|
||||||
|
|
||||||
```
|
```
|
||||||
"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner
|
"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner
|
||||||
that is making this Agreement with Wiretrustee. For legal entities, the entity making a Contribution and all other
|
that is making this Agreement with NetBird. For legal entities, the entity making a Contribution and all other
|
||||||
entities that control, are controlled by, or are under common control with that entity are considered
|
entities that control, are controlled by, or are under common control with that entity are considered
|
||||||
to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect,
|
to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect,
|
||||||
to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty
|
to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty
|
||||||
@@ -91,23 +91,23 @@ You reserve all right, title, and interest in and to Your Contributions.
|
|||||||
```
|
```
|
||||||
```
|
```
|
||||||
"Contribution" shall mean any original work of authorship, including any modifications or additions to
|
"Contribution" shall mean any original work of authorship, including any modifications or additions to
|
||||||
an existing work, that is or previously has been intentionally submitted by You to Wiretrustee for inclusion in,
|
an existing work, that is or previously has been intentionally submitted by You to NetBird for inclusion in,
|
||||||
or documentation of, any of the products owned or managed by Wiretrustee (the "Work").
|
or documentation of, any of the products owned or managed by NetBird (the "Work").
|
||||||
For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication
|
For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication
|
||||||
sent to Wiretrustee or its representatives, including but not limited to communication on electronic mailing lists,
|
sent to NetBird or its representatives, including but not limited to communication on electronic mailing lists,
|
||||||
source code control systems, and issue tracking systems that are managed by, or on behalf of,
|
source code control systems, and issue tracking systems that are managed by, or on behalf of,
|
||||||
Wiretrustee for the purpose of discussing and improving the Work, but excluding communication that is conspicuously
|
NetBird for the purpose of discussing and improving the Work, but excluding communication that is conspicuously
|
||||||
marked or otherwise designated in writing by You as "Not a Contribution."
|
marked or otherwise designated in writing by You as "Not a Contribution."
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Wiretrustee
|
2. Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to NetBird
|
||||||
and to recipients of software distributed by Wiretrustee a perpetual, worldwide, non-exclusive, no-charge,
|
and to recipients of software distributed by NetBird a perpetual, worldwide, non-exclusive, no-charge,
|
||||||
royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly
|
royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly
|
||||||
perform, sublicense, and distribute Your Contributions and such derivative works.
|
perform, sublicense, and distribute Your Contributions and such derivative works.
|
||||||
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Wiretrustee and
|
3. Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to NetBird and
|
||||||
to recipients of software distributed by Wiretrustee a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
to recipients of software distributed by NetBird a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||||
irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import,
|
irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import,
|
||||||
and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are
|
and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are
|
||||||
necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which
|
necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which
|
||||||
@@ -121,8 +121,8 @@ You reserve all right, title, and interest in and to Your Contributions.
|
|||||||
intellectual property that you create that includes your Contributions, you represent that you have received
|
intellectual property that you create that includes your Contributions, you represent that you have received
|
||||||
permission to make Contributions on behalf of that employer, that you will have received permission from your current
|
permission to make Contributions on behalf of that employer, that you will have received permission from your current
|
||||||
and future employers for all future Contributions, that your applicable employer has waived such rights for all of
|
and future employers for all future Contributions, that your applicable employer has waived such rights for all of
|
||||||
your current and future Contributions to Wiretrustee, or that your employer has executed a separate Corporate CLA
|
your current and future Contributions to NetBird, or that your employer has executed a separate Corporate CLA
|
||||||
with Wiretrustee.
|
with NetBird.
|
||||||
|
|
||||||
|
|
||||||
5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of
|
5. You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of
|
||||||
@@ -138,11 +138,11 @@ You reserve all right, title, and interest in and to Your Contributions.
|
|||||||
MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
|
MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
|
||||||
|
|
||||||
|
|
||||||
7. Should You wish to submit work that is not Your original creation, You may submit it to Wiretrustee separately from
|
7. Should You wish to submit work that is not Your original creation, You may submit it to NetBird separately from
|
||||||
any Contribution, identifying the complete details of its source and of any license or other restriction (including,
|
any Contribution, identifying the complete details of its source and of any license or other restriction (including,
|
||||||
but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and
|
but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and
|
||||||
conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
|
conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
|
||||||
|
|
||||||
|
|
||||||
8. You agree to notify Wiretrustee of any facts or circumstances of which you become aware that would make these
|
8. You agree to notify NetBird of any facts or circumstances of which you become aware that would make these
|
||||||
representations inaccurate in any respect.
|
representations inaccurate in any respect.
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
BSD 3-Clause License
|
BSD 3-Clause License
|
||||||
|
|
||||||
Copyright (c) 2022 Wiretrustee UG (haftungsbeschränkt) & AUTHORS
|
Copyright (c) 2022 NetBird GmbH & AUTHORS
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ USER netbird:netbird
|
|||||||
|
|
||||||
ENV NB_FOREGROUND_MODE=true
|
ENV NB_FOREGROUND_MODE=true
|
||||||
ENV NB_USE_NETSTACK_MODE=true
|
ENV NB_USE_NETSTACK_MODE=true
|
||||||
|
ENV NB_ENABLE_NETSTACK_LOCAL_FORWARDING=true
|
||||||
ENV NB_CONFIG=config.json
|
ENV NB_CONFIG=config.json
|
||||||
ENV NB_DAEMON_ADDR=unix://netbird.sock
|
ENV NB_DAEMON_ADDR=unix://netbird.sock
|
||||||
ENV NB_DISABLE_DNS=true
|
ENV NB_DISABLE_DNS=true
|
||||||
|
|||||||
137
client/cmd/trace.go
Normal file
137
client/cmd/trace.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
var traceCmd = &cobra.Command{
|
||||||
|
Use: "trace <direction> <source-ip> <dest-ip>",
|
||||||
|
Short: "Trace a packet through the firewall",
|
||||||
|
Example: `
|
||||||
|
netbird debug trace in 192.168.1.10 10.10.0.2 -p tcp --sport 12345 --dport 443 --syn --ack
|
||||||
|
netbird debug trace out 10.10.0.1 8.8.8.8 -p udp --dport 53
|
||||||
|
netbird debug trace in 10.10.0.2 10.10.0.1 -p icmp --type 8 --code 0
|
||||||
|
netbird debug trace in 100.64.1.1 self -p tcp --dport 80`,
|
||||||
|
Args: cobra.ExactArgs(3),
|
||||||
|
RunE: tracePacket,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
debugCmd.AddCommand(traceCmd)
|
||||||
|
|
||||||
|
traceCmd.Flags().StringP("protocol", "p", "tcp", "Protocol (tcp/udp/icmp)")
|
||||||
|
traceCmd.Flags().Uint16("sport", 0, "Source port")
|
||||||
|
traceCmd.Flags().Uint16("dport", 0, "Destination port")
|
||||||
|
traceCmd.Flags().Uint8("icmp-type", 0, "ICMP type")
|
||||||
|
traceCmd.Flags().Uint8("icmp-code", 0, "ICMP code")
|
||||||
|
traceCmd.Flags().Bool("syn", false, "TCP SYN flag")
|
||||||
|
traceCmd.Flags().Bool("ack", false, "TCP ACK flag")
|
||||||
|
traceCmd.Flags().Bool("fin", false, "TCP FIN flag")
|
||||||
|
traceCmd.Flags().Bool("rst", false, "TCP RST flag")
|
||||||
|
traceCmd.Flags().Bool("psh", false, "TCP PSH flag")
|
||||||
|
traceCmd.Flags().Bool("urg", false, "TCP URG flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
func tracePacket(cmd *cobra.Command, args []string) error {
|
||||||
|
direction := strings.ToLower(args[0])
|
||||||
|
if direction != "in" && direction != "out" {
|
||||||
|
return fmt.Errorf("invalid direction: use 'in' or 'out'")
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol := cmd.Flag("protocol").Value.String()
|
||||||
|
if protocol != "tcp" && protocol != "udp" && protocol != "icmp" {
|
||||||
|
return fmt.Errorf("invalid protocol: use tcp/udp/icmp")
|
||||||
|
}
|
||||||
|
|
||||||
|
sport, err := cmd.Flags().GetUint16("sport")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid source port: %v", err)
|
||||||
|
}
|
||||||
|
dport, err := cmd.Flags().GetUint16("dport")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid destination port: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For TCP/UDP, generate random ephemeral port (49152-65535) if not specified
|
||||||
|
if protocol != "icmp" {
|
||||||
|
if sport == 0 {
|
||||||
|
sport = uint16(rand.Intn(16383) + 49152)
|
||||||
|
}
|
||||||
|
if dport == 0 {
|
||||||
|
dport = uint16(rand.Intn(16383) + 49152)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tcpFlags *proto.TCPFlags
|
||||||
|
if protocol == "tcp" {
|
||||||
|
syn, _ := cmd.Flags().GetBool("syn")
|
||||||
|
ack, _ := cmd.Flags().GetBool("ack")
|
||||||
|
fin, _ := cmd.Flags().GetBool("fin")
|
||||||
|
rst, _ := cmd.Flags().GetBool("rst")
|
||||||
|
psh, _ := cmd.Flags().GetBool("psh")
|
||||||
|
urg, _ := cmd.Flags().GetBool("urg")
|
||||||
|
|
||||||
|
tcpFlags = &proto.TCPFlags{
|
||||||
|
Syn: syn,
|
||||||
|
Ack: ack,
|
||||||
|
Fin: fin,
|
||||||
|
Rst: rst,
|
||||||
|
Psh: psh,
|
||||||
|
Urg: urg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
icmpType, _ := cmd.Flags().GetUint32("icmp-type")
|
||||||
|
icmpCode, _ := cmd.Flags().GetUint32("icmp-code")
|
||||||
|
|
||||||
|
conn, err := getClient(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
client := proto.NewDaemonServiceClient(conn)
|
||||||
|
resp, err := client.TracePacket(cmd.Context(), &proto.TracePacketRequest{
|
||||||
|
SourceIp: args[1],
|
||||||
|
DestinationIp: args[2],
|
||||||
|
Protocol: protocol,
|
||||||
|
SourcePort: uint32(sport),
|
||||||
|
DestinationPort: uint32(dport),
|
||||||
|
Direction: direction,
|
||||||
|
TcpFlags: tcpFlags,
|
||||||
|
IcmpType: &icmpType,
|
||||||
|
IcmpCode: &icmpCode,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("trace failed: %v", status.Convert(err).Message())
|
||||||
|
}
|
||||||
|
|
||||||
|
printTrace(cmd, args[1], args[2], protocol, sport, dport, resp)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printTrace(cmd *cobra.Command, src, dst, proto string, sport, dport uint16, resp *proto.TracePacketResponse) {
|
||||||
|
cmd.Printf("Packet trace %s:%d -> %s:%d (%s)\n\n", src, sport, dst, dport, strings.ToUpper(proto))
|
||||||
|
|
||||||
|
for _, stage := range resp.Stages {
|
||||||
|
if stage.ForwardingDetails != nil {
|
||||||
|
cmd.Printf("%s: %s [%s]\n", stage.Name, stage.Message, *stage.ForwardingDetails)
|
||||||
|
} else {
|
||||||
|
cmd.Printf("%s: %s\n", stage.Name, stage.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disposition := map[bool]string{
|
||||||
|
true: "\033[32mALLOWED\033[0m", // Green
|
||||||
|
false: "\033[31mDENIED\033[0m", // Red
|
||||||
|
}[resp.FinalDisposition]
|
||||||
|
|
||||||
|
cmd.Printf("\nFinal disposition: %s\n", disposition)
|
||||||
|
}
|
||||||
@@ -190,7 +190,7 @@ func runInForegroundMode(ctx context.Context, cmd *cobra.Command) error {
|
|||||||
r.GetFullStatus()
|
r.GetFullStatus()
|
||||||
|
|
||||||
connectClient := internal.NewConnectClient(ctx, config, r)
|
connectClient := internal.NewConnectClient(ctx, config, r)
|
||||||
return connectClient.Run()
|
return connectClient.Run(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
func runInDaemonMode(ctx context.Context, cmd *cobra.Command) error {
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewFirewall creates a firewall manager instance
|
// NewFirewall creates a firewall manager instance
|
||||||
func NewFirewall(iface IFaceMapper, _ *statemanager.Manager) (firewall.Manager, error) {
|
func NewFirewall(iface IFaceMapper, _ *statemanager.Manager, disableServerRoutes bool) (firewall.Manager, error) {
|
||||||
if !iface.IsUserspaceBind() {
|
if !iface.IsUserspaceBind() {
|
||||||
return nil, fmt.Errorf("not implemented for this OS: %s", runtime.GOOS)
|
return nil, fmt.Errorf("not implemented for this OS: %s", runtime.GOOS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// use userspace packet filtering firewall
|
// use userspace packet filtering firewall
|
||||||
fm, err := uspfilter.Create(iface)
|
fm, err := uspfilter.Create(iface, disableServerRoutes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ const SKIP_NFTABLES_ENV = "NB_SKIP_NFTABLES_CHECK"
|
|||||||
// FWType is the type for the firewall type
|
// FWType is the type for the firewall type
|
||||||
type FWType int
|
type FWType int
|
||||||
|
|
||||||
func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager) (firewall.Manager, error) {
|
func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager, disableServerRoutes bool) (firewall.Manager, error) {
|
||||||
// on the linux system we try to user nftables or iptables
|
// on the linux system we try to user nftables or iptables
|
||||||
// in any case, because we need to allow netbird interface traffic
|
// in any case, because we need to allow netbird interface traffic
|
||||||
// so we use AllowNetbird traffic from these firewall managers
|
// so we use AllowNetbird traffic from these firewall managers
|
||||||
// for the userspace packet filtering firewall
|
// for the userspace packet filtering firewall
|
||||||
fm, err := createNativeFirewall(iface, stateManager)
|
fm, err := createNativeFirewall(iface, stateManager, disableServerRoutes)
|
||||||
|
|
||||||
if !iface.IsUserspaceBind() {
|
if !iface.IsUserspaceBind() {
|
||||||
return fm, err
|
return fm, err
|
||||||
@@ -47,10 +47,10 @@ func NewFirewall(iface IFaceMapper, stateManager *statemanager.Manager) (firewal
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("failed to create native firewall: %v. Proceeding with userspace", err)
|
log.Warnf("failed to create native firewall: %v. Proceeding with userspace", err)
|
||||||
}
|
}
|
||||||
return createUserspaceFirewall(iface, fm)
|
return createUserspaceFirewall(iface, fm, disableServerRoutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createNativeFirewall(iface IFaceMapper, stateManager *statemanager.Manager) (firewall.Manager, error) {
|
func createNativeFirewall(iface IFaceMapper, stateManager *statemanager.Manager, routes bool) (firewall.Manager, error) {
|
||||||
fm, err := createFW(iface)
|
fm, err := createFW(iface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create firewall: %s", err)
|
return nil, fmt.Errorf("create firewall: %s", err)
|
||||||
@@ -77,12 +77,12 @@ func createFW(iface IFaceMapper) (firewall.Manager, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createUserspaceFirewall(iface IFaceMapper, fm firewall.Manager) (firewall.Manager, error) {
|
func createUserspaceFirewall(iface IFaceMapper, fm firewall.Manager, disableServerRoutes bool) (firewall.Manager, error) {
|
||||||
var errUsp error
|
var errUsp error
|
||||||
if fm != nil {
|
if fm != nil {
|
||||||
fm, errUsp = uspfilter.CreateWithNativeFirewall(iface, fm)
|
fm, errUsp = uspfilter.CreateWithNativeFirewall(iface, fm, disableServerRoutes)
|
||||||
} else {
|
} else {
|
||||||
fm, errUsp = uspfilter.Create(iface)
|
fm, errUsp = uspfilter.Create(iface, disableServerRoutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
if errUsp != nil {
|
if errUsp != nil {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package firewall
|
package firewall
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -10,4 +12,6 @@ type IFaceMapper interface {
|
|||||||
Address() device.WGAddress
|
Address() device.WGAddress
|
||||||
IsUserspaceBind() bool
|
IsUserspaceBind() bool
|
||||||
SetFilter(device.PacketFilter) error
|
SetFilter(device.PacketFilter) error
|
||||||
|
GetDevice() *device.FilteredDevice
|
||||||
|
GetWGDevice() *wgdevice.Device
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package iptables
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"slices"
|
||||||
|
|
||||||
"github.com/coreos/go-iptables/iptables"
|
"github.com/coreos/go-iptables/iptables"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -86,19 +86,19 @@ func (m *aclManager) AddPeerFiltering(
|
|||||||
action firewall.Action,
|
action firewall.Action,
|
||||||
ipsetName string,
|
ipsetName string,
|
||||||
) ([]firewall.Rule, error) {
|
) ([]firewall.Rule, error) {
|
||||||
var dPortVal, sPortVal string
|
|
||||||
if dPort != nil && dPort.Values != nil {
|
|
||||||
// TODO: we support only one port per rule in current implementation of ACLs
|
|
||||||
dPortVal = strconv.Itoa(dPort.Values[0])
|
|
||||||
}
|
|
||||||
if sPort != nil && sPort.Values != nil {
|
|
||||||
sPortVal = strconv.Itoa(sPort.Values[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
chain := chainNameInputRules
|
chain := chainNameInputRules
|
||||||
|
|
||||||
ipsetName = transformIPsetName(ipsetName, sPortVal, dPortVal)
|
ipsetName = transformIPsetName(ipsetName, sPort, dPort)
|
||||||
specs := filterRuleSpecs(ip, string(protocol), sPortVal, dPortVal, action, ipsetName)
|
specs := filterRuleSpecs(ip, string(protocol), sPort, dPort, action, ipsetName)
|
||||||
|
|
||||||
|
mangleSpecs := slices.Clone(specs)
|
||||||
|
mangleSpecs = append(mangleSpecs,
|
||||||
|
"-i", m.wgIface.Name(),
|
||||||
|
"-m", "addrtype", "--dst-type", "LOCAL",
|
||||||
|
"-j", "MARK", "--set-xmark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected),
|
||||||
|
)
|
||||||
|
|
||||||
|
specs = append(specs, "-j", actionToStr(action))
|
||||||
if ipsetName != "" {
|
if ipsetName != "" {
|
||||||
if ipList, ipsetExists := m.ipsetStore.ipset(ipsetName); ipsetExists {
|
if ipList, ipsetExists := m.ipsetStore.ipset(ipsetName); ipsetExists {
|
||||||
if err := ipset.Add(ipsetName, ip.String()); err != nil {
|
if err := ipset.Add(ipsetName, ip.String()); err != nil {
|
||||||
@@ -130,7 +130,7 @@ func (m *aclManager) AddPeerFiltering(
|
|||||||
m.ipsetStore.addIpList(ipsetName, ipList)
|
m.ipsetStore.addIpList(ipsetName, ipList)
|
||||||
}
|
}
|
||||||
|
|
||||||
ok, err := m.iptablesClient.Exists("filter", chain, specs...)
|
ok, err := m.iptablesClient.Exists(tableFilter, chain, specs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to check rule: %w", err)
|
return nil, fmt.Errorf("failed to check rule: %w", err)
|
||||||
}
|
}
|
||||||
@@ -138,13 +138,19 @@ func (m *aclManager) AddPeerFiltering(
|
|||||||
return nil, fmt.Errorf("rule already exists")
|
return nil, fmt.Errorf("rule already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.iptablesClient.Append("filter", chain, specs...); err != nil {
|
if err := m.iptablesClient.Append(tableFilter, chain, specs...); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := m.iptablesClient.Append(tableMangle, chainRTPRE, mangleSpecs...); err != nil {
|
||||||
|
log.Errorf("failed to add mangle rule: %v", err)
|
||||||
|
mangleSpecs = nil
|
||||||
|
}
|
||||||
|
|
||||||
rule := &Rule{
|
rule := &Rule{
|
||||||
ruleID: uuid.New().String(),
|
ruleID: uuid.New().String(),
|
||||||
specs: specs,
|
specs: specs,
|
||||||
|
mangleSpecs: mangleSpecs,
|
||||||
ipsetName: ipsetName,
|
ipsetName: ipsetName,
|
||||||
ip: ip.String(),
|
ip: ip.String(),
|
||||||
chain: chain,
|
chain: chain,
|
||||||
@@ -190,6 +196,12 @@ func (m *aclManager) DeletePeerRule(rule firewall.Rule) error {
|
|||||||
return fmt.Errorf("failed to delete rule: %s, %v: %w", r.chain, r.specs, err)
|
return fmt.Errorf("failed to delete rule: %s, %v: %w", r.chain, r.specs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if r.mangleSpecs != nil {
|
||||||
|
if err := m.iptablesClient.Delete(tableMangle, chainRTPRE, r.mangleSpecs...); err != nil {
|
||||||
|
log.Errorf("failed to delete mangle rule: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m.updateState()
|
m.updateState()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -310,17 +322,10 @@ func (m *aclManager) seedInitialEntries() {
|
|||||||
func (m *aclManager) seedInitialOptionalEntries() {
|
func (m *aclManager) seedInitialOptionalEntries() {
|
||||||
m.optionalEntries["FORWARD"] = []entry{
|
m.optionalEntries["FORWARD"] = []entry{
|
||||||
{
|
{
|
||||||
spec: []string{"-m", "mark", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected), "-j", chainNameInputRules},
|
spec: []string{"-m", "mark", "--mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected), "-j", "ACCEPT"},
|
||||||
position: 2,
|
position: 2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
m.optionalEntries["PREROUTING"] = []entry{
|
|
||||||
{
|
|
||||||
spec: []string{"-t", "mangle", "-i", m.wgIface.Name(), "-m", "addrtype", "--dst-type", "LOCAL", "-j", "MARK", "--set-mark", fmt.Sprintf("%#x", nbnet.PreroutingFwmarkRedirected)},
|
|
||||||
position: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *aclManager) appendToEntries(chainName string, spec []string) {
|
func (m *aclManager) appendToEntries(chainName string, spec []string) {
|
||||||
@@ -354,7 +359,7 @@ func (m *aclManager) updateState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// filterRuleSpecs returns the specs of a filtering rule
|
// filterRuleSpecs returns the specs of a filtering rule
|
||||||
func filterRuleSpecs(ip net.IP, protocol, sPort, dPort string, action firewall.Action, ipsetName string) (specs []string) {
|
func filterRuleSpecs(ip net.IP, protocol string, sPort, dPort *firewall.Port, action firewall.Action, ipsetName string) (specs []string) {
|
||||||
matchByIP := true
|
matchByIP := true
|
||||||
// don't use IP matching if IP is ip 0.0.0.0
|
// don't use IP matching if IP is ip 0.0.0.0
|
||||||
if ip.String() == "0.0.0.0" {
|
if ip.String() == "0.0.0.0" {
|
||||||
@@ -371,13 +376,9 @@ func filterRuleSpecs(ip net.IP, protocol, sPort, dPort string, action firewall.A
|
|||||||
if protocol != "all" {
|
if protocol != "all" {
|
||||||
specs = append(specs, "-p", protocol)
|
specs = append(specs, "-p", protocol)
|
||||||
}
|
}
|
||||||
if sPort != "" {
|
specs = append(specs, applyPort("--sport", sPort)...)
|
||||||
specs = append(specs, "--sport", sPort)
|
specs = append(specs, applyPort("--dport", dPort)...)
|
||||||
}
|
return specs
|
||||||
if dPort != "" {
|
|
||||||
specs = append(specs, "--dport", dPort)
|
|
||||||
}
|
|
||||||
return append(specs, "-j", actionToStr(action))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func actionToStr(action firewall.Action) string {
|
func actionToStr(action firewall.Action) string {
|
||||||
@@ -387,15 +388,15 @@ func actionToStr(action firewall.Action) string {
|
|||||||
return "DROP"
|
return "DROP"
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformIPsetName(ipsetName string, sPort, dPort string) string {
|
func transformIPsetName(ipsetName string, sPort, dPort *firewall.Port) string {
|
||||||
switch {
|
switch {
|
||||||
case ipsetName == "":
|
case ipsetName == "":
|
||||||
return ""
|
return ""
|
||||||
case sPort != "" && dPort != "":
|
case sPort != nil && dPort != nil:
|
||||||
return ipsetName + "-sport-dport"
|
return ipsetName + "-sport-dport"
|
||||||
case sPort != "":
|
case sPort != nil:
|
||||||
return ipsetName + "-sport"
|
return ipsetName + "-sport"
|
||||||
case dPort != "":
|
case dPort != nil:
|
||||||
return ipsetName + "-dport"
|
return ipsetName + "-dport"
|
||||||
default:
|
default:
|
||||||
return ipsetName
|
return ipsetName
|
||||||
|
|||||||
@@ -213,6 +213,19 @@ func (m *Manager) AllowNetbird() error {
|
|||||||
// Flush doesn't need to be implemented for this manager
|
// Flush doesn't need to be implemented for this manager
|
||||||
func (m *Manager) Flush() error { return nil }
|
func (m *Manager) Flush() error { return nil }
|
||||||
|
|
||||||
|
// SetLogLevel sets the log level for the firewall manager
|
||||||
|
func (m *Manager) SetLogLevel(log.Level) {
|
||||||
|
// not supported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) EnableRouting() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) DisableRouting() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getConntrackEstablished() []string {
|
func getConntrackEstablished() []string {
|
||||||
return []string{"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"}
|
return []string{"-m", "conntrack", "--ctstate", "RELATED,ESTABLISHED", "-j", "ACCEPT"}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ func TestIptablesManager(t *testing.T) {
|
|||||||
t.Run("add second rule", func(t *testing.T) {
|
t.Run("add second rule", func(t *testing.T) {
|
||||||
ip := net.ParseIP("10.20.0.3")
|
ip := net.ParseIP("10.20.0.3")
|
||||||
port := &fw.Port{
|
port := &fw.Port{
|
||||||
Values: []int{8043: 8046},
|
IsRange: true,
|
||||||
|
Values: []uint16{8043, 8046},
|
||||||
}
|
}
|
||||||
rule2, err = manager.AddPeerFiltering(ip, "tcp", port, nil, fw.ActionAccept, "", "accept HTTPS traffic from ports range")
|
rule2, err = manager.AddPeerFiltering(ip, "tcp", port, nil, fw.ActionAccept, "", "accept HTTPS traffic from ports range")
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
@@ -95,7 +96,7 @@ func TestIptablesManager(t *testing.T) {
|
|||||||
t.Run("reset check", func(t *testing.T) {
|
t.Run("reset check", func(t *testing.T) {
|
||||||
// add second rule
|
// add second rule
|
||||||
ip := net.ParseIP("10.20.0.3")
|
ip := net.ParseIP("10.20.0.3")
|
||||||
port := &fw.Port{Values: []int{5353}}
|
port := &fw.Port{Values: []uint16{5353}}
|
||||||
_, err = manager.AddPeerFiltering(ip, "udp", nil, port, fw.ActionAccept, "", "accept Fake DNS traffic")
|
_, err = manager.AddPeerFiltering(ip, "udp", nil, port, fw.ActionAccept, "", "accept Fake DNS traffic")
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
|
|
||||||
@@ -145,7 +146,7 @@ func TestIptablesManagerIPSet(t *testing.T) {
|
|||||||
t.Run("add second rule", func(t *testing.T) {
|
t.Run("add second rule", func(t *testing.T) {
|
||||||
ip := net.ParseIP("10.20.0.3")
|
ip := net.ParseIP("10.20.0.3")
|
||||||
port := &fw.Port{
|
port := &fw.Port{
|
||||||
Values: []int{443},
|
Values: []uint16{443},
|
||||||
}
|
}
|
||||||
rule2, err = manager.AddPeerFiltering(ip, "tcp", port, nil, fw.ActionAccept, "default", "accept HTTPS traffic from ports range")
|
rule2, err = manager.AddPeerFiltering(ip, "tcp", port, nil, fw.ActionAccept, "default", "accept HTTPS traffic from ports range")
|
||||||
for _, r := range rule2 {
|
for _, r := range rule2 {
|
||||||
@@ -214,7 +215,7 @@ func TestIptablesCreatePerformance(t *testing.T) {
|
|||||||
ip := net.ParseIP("10.20.0.100")
|
ip := net.ParseIP("10.20.0.100")
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
for i := 0; i < testMax; i++ {
|
for i := 0; i < testMax; i++ {
|
||||||
port := &fw.Port{Values: []int{1000 + i}}
|
port := &fw.Port{Values: []uint16{uint16(1000 + i)}}
|
||||||
_, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic")
|
_, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic")
|
||||||
|
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
|
|||||||
@@ -135,7 +135,16 @@ func (r *router) AddRouteFiltering(
|
|||||||
}
|
}
|
||||||
|
|
||||||
rule := genRouteFilteringRuleSpec(params)
|
rule := genRouteFilteringRuleSpec(params)
|
||||||
if err := r.iptablesClient.Append(tableFilter, chainRTFWD, rule...); err != nil {
|
// Insert DROP rules at the beginning, append ACCEPT rules at the end
|
||||||
|
var err error
|
||||||
|
if action == firewall.ActionDrop {
|
||||||
|
// after the established rule
|
||||||
|
err = r.iptablesClient.Insert(tableFilter, chainRTFWD, 2, rule...)
|
||||||
|
} else {
|
||||||
|
err = r.iptablesClient.Append(tableFilter, chainRTFWD, rule...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
return nil, fmt.Errorf("add route rule: %v", err)
|
return nil, fmt.Errorf("add route rule: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -590,10 +599,10 @@ func applyPort(flag string, port *firewall.Port) []string {
|
|||||||
if len(port.Values) > 1 {
|
if len(port.Values) > 1 {
|
||||||
portList := make([]string, len(port.Values))
|
portList := make([]string, len(port.Values))
|
||||||
for i, p := range port.Values {
|
for i, p := range port.Values {
|
||||||
portList[i] = strconv.Itoa(p)
|
portList[i] = strconv.Itoa(int(p))
|
||||||
}
|
}
|
||||||
return []string{"-m", "multiport", flag, strings.Join(portList, ",")}
|
return []string{"-m", "multiport", flag, strings.Join(portList, ",")}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []string{flag, strconv.Itoa(port.Values[0])}
|
return []string{flag, strconv.Itoa(int(port.Values[0]))}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
destination: netip.MustParsePrefix("10.0.0.0/24"),
|
destination: netip.MustParsePrefix("10.0.0.0/24"),
|
||||||
proto: firewall.ProtocolTCP,
|
proto: firewall.ProtocolTCP,
|
||||||
sPort: nil,
|
sPort: nil,
|
||||||
dPort: &firewall.Port{Values: []int{80}},
|
dPort: &firewall.Port{Values: []uint16{80}},
|
||||||
direction: firewall.RuleDirectionIN,
|
direction: firewall.RuleDirectionIN,
|
||||||
action: firewall.ActionAccept,
|
action: firewall.ActionAccept,
|
||||||
expectSet: false,
|
expectSet: false,
|
||||||
@@ -252,7 +252,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
},
|
},
|
||||||
destination: netip.MustParsePrefix("10.0.0.0/8"),
|
destination: netip.MustParsePrefix("10.0.0.0/8"),
|
||||||
proto: firewall.ProtocolUDP,
|
proto: firewall.ProtocolUDP,
|
||||||
sPort: &firewall.Port{Values: []int{1024, 2048}, IsRange: true},
|
sPort: &firewall.Port{Values: []uint16{1024, 2048}, IsRange: true},
|
||||||
dPort: nil,
|
dPort: nil,
|
||||||
direction: firewall.RuleDirectionOUT,
|
direction: firewall.RuleDirectionOUT,
|
||||||
action: firewall.ActionDrop,
|
action: firewall.ActionDrop,
|
||||||
@@ -285,7 +285,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
sources: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/12")},
|
sources: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/12")},
|
||||||
destination: netip.MustParsePrefix("192.168.0.0/16"),
|
destination: netip.MustParsePrefix("192.168.0.0/16"),
|
||||||
proto: firewall.ProtocolTCP,
|
proto: firewall.ProtocolTCP,
|
||||||
sPort: &firewall.Port{Values: []int{80, 443, 8080}},
|
sPort: &firewall.Port{Values: []uint16{80, 443, 8080}},
|
||||||
dPort: nil,
|
dPort: nil,
|
||||||
direction: firewall.RuleDirectionOUT,
|
direction: firewall.RuleDirectionOUT,
|
||||||
action: firewall.ActionAccept,
|
action: firewall.ActionAccept,
|
||||||
@@ -297,7 +297,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
destination: netip.MustParsePrefix("10.0.0.0/24"),
|
destination: netip.MustParsePrefix("10.0.0.0/24"),
|
||||||
proto: firewall.ProtocolUDP,
|
proto: firewall.ProtocolUDP,
|
||||||
sPort: nil,
|
sPort: nil,
|
||||||
dPort: &firewall.Port{Values: []int{5000, 5100}, IsRange: true},
|
dPort: &firewall.Port{Values: []uint16{5000, 5100}, IsRange: true},
|
||||||
direction: firewall.RuleDirectionIN,
|
direction: firewall.RuleDirectionIN,
|
||||||
action: firewall.ActionDrop,
|
action: firewall.ActionDrop,
|
||||||
expectSet: false,
|
expectSet: false,
|
||||||
@@ -307,8 +307,8 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
sources: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")},
|
sources: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")},
|
||||||
destination: netip.MustParsePrefix("172.16.0.0/16"),
|
destination: netip.MustParsePrefix("172.16.0.0/16"),
|
||||||
proto: firewall.ProtocolTCP,
|
proto: firewall.ProtocolTCP,
|
||||||
sPort: &firewall.Port{Values: []int{1024, 65535}, IsRange: true},
|
sPort: &firewall.Port{Values: []uint16{1024, 65535}, IsRange: true},
|
||||||
dPort: &firewall.Port{Values: []int{22}},
|
dPort: &firewall.Port{Values: []uint16{22}},
|
||||||
direction: firewall.RuleDirectionOUT,
|
direction: firewall.RuleDirectionOUT,
|
||||||
action: firewall.ActionAccept,
|
action: firewall.ActionAccept,
|
||||||
expectSet: false,
|
expectSet: false,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ type Rule struct {
|
|||||||
ipsetName string
|
ipsetName string
|
||||||
|
|
||||||
specs []string
|
specs []string
|
||||||
|
mangleSpecs []string
|
||||||
ip string
|
ip string
|
||||||
chain string
|
chain string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,6 +99,12 @@ type Manager interface {
|
|||||||
|
|
||||||
// Flush the changes to firewall controller
|
// Flush the changes to firewall controller
|
||||||
Flush() error
|
Flush() error
|
||||||
|
|
||||||
|
SetLogLevel(log.Level)
|
||||||
|
|
||||||
|
EnableRouting() error
|
||||||
|
|
||||||
|
DisableRouting() error
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenKey(format string, pair RouterPair) string {
|
func GenKey(format string, pair RouterPair) string {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ type Port struct {
|
|||||||
IsRange bool
|
IsRange bool
|
||||||
|
|
||||||
// Values contains one value for single port, multiple values for the list of ports, or two values for the range of ports
|
// Values contains one value for single port, multiple values for the list of ports, or two values for the range of ports
|
||||||
Values []int
|
Values []uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
// String interface implementation
|
// String interface implementation
|
||||||
@@ -40,7 +40,11 @@ func (p *Port) String() string {
|
|||||||
if ports != "" {
|
if ports != "" {
|
||||||
ports += ","
|
ports += ","
|
||||||
}
|
}
|
||||||
ports += strconv.Itoa(port)
|
ports += strconv.Itoa(int(port))
|
||||||
}
|
}
|
||||||
|
if p.IsRange {
|
||||||
|
ports = "range:" + ports
|
||||||
|
}
|
||||||
|
|
||||||
return ports
|
return ports
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package nftables
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -46,6 +46,7 @@ type AclManager struct {
|
|||||||
|
|
||||||
workTable *nftables.Table
|
workTable *nftables.Table
|
||||||
chainInputRules *nftables.Chain
|
chainInputRules *nftables.Chain
|
||||||
|
chainPrerouting *nftables.Chain
|
||||||
|
|
||||||
ipsetStore *ipsetStore
|
ipsetStore *ipsetStore
|
||||||
rules map[string]*Rule
|
rules map[string]*Rule
|
||||||
@@ -118,23 +119,32 @@ func (m *AclManager) DeletePeerRule(rule firewall.Rule) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if r.nftSet == nil {
|
if r.nftSet == nil {
|
||||||
err := m.rConn.DelRule(r.nftRule)
|
if err := m.rConn.DelRule(r.nftRule); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to delete rule: %v", err)
|
log.Errorf("failed to delete rule: %v", err)
|
||||||
}
|
}
|
||||||
|
if r.mangleRule != nil {
|
||||||
|
if err := m.rConn.DelRule(r.mangleRule); err != nil {
|
||||||
|
log.Errorf("failed to delete mangle rule: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
delete(m.rules, r.GetRuleID())
|
delete(m.rules, r.GetRuleID())
|
||||||
return m.rConn.Flush()
|
return m.rConn.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
ips, ok := m.ipsetStore.ips(r.nftSet.Name)
|
ips, ok := m.ipsetStore.ips(r.nftSet.Name)
|
||||||
if !ok {
|
if !ok {
|
||||||
err := m.rConn.DelRule(r.nftRule)
|
if err := m.rConn.DelRule(r.nftRule); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to delete rule: %v", err)
|
log.Errorf("failed to delete rule: %v", err)
|
||||||
}
|
}
|
||||||
|
if r.mangleRule != nil {
|
||||||
|
if err := m.rConn.DelRule(r.mangleRule); err != nil {
|
||||||
|
log.Errorf("failed to delete mangle rule: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
delete(m.rules, r.GetRuleID())
|
delete(m.rules, r.GetRuleID())
|
||||||
return m.rConn.Flush()
|
return m.rConn.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := ips[r.ip.String()]; ok {
|
if _, ok := ips[r.ip.String()]; ok {
|
||||||
err := m.sConn.SetDeleteElements(r.nftSet, []nftables.SetElement{{Key: r.ip.To4()}})
|
err := m.sConn.SetDeleteElements(r.nftSet, []nftables.SetElement{{Key: r.ip.To4()}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -153,12 +163,16 @@ func (m *AclManager) DeletePeerRule(rule firewall.Rule) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := m.rConn.DelRule(r.nftRule)
|
if err := m.rConn.DelRule(r.nftRule); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Errorf("failed to delete rule: %v", err)
|
log.Errorf("failed to delete rule: %v", err)
|
||||||
}
|
}
|
||||||
err = m.rConn.Flush()
|
if r.mangleRule != nil {
|
||||||
if err != nil {
|
if err := m.rConn.DelRule(r.mangleRule); err != nil {
|
||||||
|
log.Errorf("failed to delete mangle rule: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.rConn.Flush(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,9 +239,12 @@ func (m *AclManager) Flush() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.refreshRuleHandles(m.chainInputRules); err != nil {
|
if err := m.refreshRuleHandles(m.chainInputRules, false); err != nil {
|
||||||
log.Errorf("failed to refresh rule handles ipv4 input chain: %v", err)
|
log.Errorf("failed to refresh rule handles ipv4 input chain: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := m.refreshRuleHandles(m.chainPrerouting, true); err != nil {
|
||||||
|
log.Errorf("failed to refresh rule handles prerouting chain: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -244,10 +261,11 @@ func (m *AclManager) addIOFiltering(
|
|||||||
ruleId := generatePeerRuleId(ip, sPort, dPort, action, ipset)
|
ruleId := generatePeerRuleId(ip, sPort, dPort, action, ipset)
|
||||||
if r, ok := m.rules[ruleId]; ok {
|
if r, ok := m.rules[ruleId]; ok {
|
||||||
return &Rule{
|
return &Rule{
|
||||||
r.nftRule,
|
nftRule: r.nftRule,
|
||||||
r.nftSet,
|
mangleRule: r.mangleRule,
|
||||||
r.ruleID,
|
nftSet: r.nftSet,
|
||||||
ip,
|
ruleID: r.ruleID,
|
||||||
|
ip: ip,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,43 +326,16 @@ func (m *AclManager) addIOFiltering(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sPort != nil && len(sPort.Values) != 0 {
|
expressions = append(expressions, applyPort(sPort, true)...)
|
||||||
expressions = append(expressions,
|
expressions = append(expressions, applyPort(dPort, false)...)
|
||||||
&expr.Payload{
|
|
||||||
DestRegister: 1,
|
|
||||||
Base: expr.PayloadBaseTransportHeader,
|
|
||||||
Offset: 0,
|
|
||||||
Len: 2,
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: encodePort(*sPort),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dPort != nil && len(dPort.Values) != 0 {
|
mainExpressions := slices.Clone(expressions)
|
||||||
expressions = append(expressions,
|
|
||||||
&expr.Payload{
|
|
||||||
DestRegister: 1,
|
|
||||||
Base: expr.PayloadBaseTransportHeader,
|
|
||||||
Offset: 2,
|
|
||||||
Len: 2,
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: encodePort(*dPort),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch action {
|
switch action {
|
||||||
case firewall.ActionAccept:
|
case firewall.ActionAccept:
|
||||||
expressions = append(expressions, &expr.Verdict{Kind: expr.VerdictAccept})
|
mainExpressions = append(mainExpressions, &expr.Verdict{Kind: expr.VerdictAccept})
|
||||||
case firewall.ActionDrop:
|
case firewall.ActionDrop:
|
||||||
expressions = append(expressions, &expr.Verdict{Kind: expr.VerdictDrop})
|
mainExpressions = append(mainExpressions, &expr.Verdict{Kind: expr.VerdictDrop})
|
||||||
}
|
}
|
||||||
|
|
||||||
userData := []byte(strings.Join([]string{ruleId, comment}, " "))
|
userData := []byte(strings.Join([]string{ruleId, comment}, " "))
|
||||||
@@ -353,12 +344,17 @@ func (m *AclManager) addIOFiltering(
|
|||||||
nftRule := m.rConn.AddRule(&nftables.Rule{
|
nftRule := m.rConn.AddRule(&nftables.Rule{
|
||||||
Table: m.workTable,
|
Table: m.workTable,
|
||||||
Chain: chain,
|
Chain: chain,
|
||||||
Exprs: expressions,
|
Exprs: mainExpressions,
|
||||||
UserData: userData,
|
UserData: userData,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if err := m.rConn.Flush(); err != nil {
|
||||||
|
return nil, fmt.Errorf(flushError, err)
|
||||||
|
}
|
||||||
|
|
||||||
rule := &Rule{
|
rule := &Rule{
|
||||||
nftRule: nftRule,
|
nftRule: nftRule,
|
||||||
|
mangleRule: m.createPreroutingRule(expressions, userData),
|
||||||
nftSet: ipset,
|
nftSet: ipset,
|
||||||
ruleID: ruleId,
|
ruleID: ruleId,
|
||||||
ip: ip,
|
ip: ip,
|
||||||
@@ -367,9 +363,63 @@ func (m *AclManager) addIOFiltering(
|
|||||||
if ipset != nil {
|
if ipset != nil {
|
||||||
m.ipsetStore.AddReferenceToIpset(ipset.Name)
|
m.ipsetStore.AddReferenceToIpset(ipset.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
return rule, nil
|
return rule, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *AclManager) createPreroutingRule(expressions []expr.Any, userData []byte) *nftables.Rule {
|
||||||
|
if m.chainPrerouting == nil {
|
||||||
|
log.Warn("prerouting chain is not created")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
preroutingExprs := slices.Clone(expressions)
|
||||||
|
|
||||||
|
// interface
|
||||||
|
preroutingExprs = append([]expr.Any{
|
||||||
|
&expr.Meta{
|
||||||
|
Key: expr.MetaKeyIIFNAME,
|
||||||
|
Register: 1,
|
||||||
|
},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpEq,
|
||||||
|
Register: 1,
|
||||||
|
Data: ifname(m.wgIface.Name()),
|
||||||
|
},
|
||||||
|
}, preroutingExprs...)
|
||||||
|
|
||||||
|
// local destination and mark
|
||||||
|
preroutingExprs = append(preroutingExprs,
|
||||||
|
&expr.Fib{
|
||||||
|
Register: 1,
|
||||||
|
ResultADDRTYPE: true,
|
||||||
|
FlagDADDR: true,
|
||||||
|
},
|
||||||
|
&expr.Cmp{
|
||||||
|
Op: expr.CmpOpEq,
|
||||||
|
Register: 1,
|
||||||
|
Data: binaryutil.NativeEndian.PutUint32(unix.RTN_LOCAL),
|
||||||
|
},
|
||||||
|
|
||||||
|
&expr.Immediate{
|
||||||
|
Register: 1,
|
||||||
|
Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkRedirected),
|
||||||
|
},
|
||||||
|
&expr.Meta{
|
||||||
|
Key: expr.MetaKeyMARK,
|
||||||
|
Register: 1,
|
||||||
|
SourceRegister: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return m.rConn.AddRule(&nftables.Rule{
|
||||||
|
Table: m.workTable,
|
||||||
|
Chain: m.chainPrerouting,
|
||||||
|
Exprs: preroutingExprs,
|
||||||
|
UserData: userData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (m *AclManager) createDefaultChains() (err error) {
|
func (m *AclManager) createDefaultChains() (err error) {
|
||||||
// chainNameInputRules
|
// chainNameInputRules
|
||||||
chain := m.createChain(chainNameInputRules)
|
chain := m.createChain(chainNameInputRules)
|
||||||
@@ -413,7 +463,7 @@ func (m *AclManager) createDefaultChains() (err error) {
|
|||||||
// go through the input filter as well. This will enable e.g. Docker services to keep working by accessing the
|
// go through the input filter as well. This will enable e.g. Docker services to keep working by accessing the
|
||||||
// netbird peer IP.
|
// netbird peer IP.
|
||||||
func (m *AclManager) allowRedirectedTraffic(chainFwFilter *nftables.Chain) error {
|
func (m *AclManager) allowRedirectedTraffic(chainFwFilter *nftables.Chain) error {
|
||||||
preroutingChain := m.rConn.AddChain(&nftables.Chain{
|
m.chainPrerouting = m.rConn.AddChain(&nftables.Chain{
|
||||||
Name: chainNamePrerouting,
|
Name: chainNamePrerouting,
|
||||||
Table: m.workTable,
|
Table: m.workTable,
|
||||||
Type: nftables.ChainTypeFilter,
|
Type: nftables.ChainTypeFilter,
|
||||||
@@ -421,8 +471,6 @@ func (m *AclManager) allowRedirectedTraffic(chainFwFilter *nftables.Chain) error
|
|||||||
Priority: nftables.ChainPriorityMangle,
|
Priority: nftables.ChainPriorityMangle,
|
||||||
})
|
})
|
||||||
|
|
||||||
m.addPreroutingRule(preroutingChain)
|
|
||||||
|
|
||||||
m.addFwmarkToForward(chainFwFilter)
|
m.addFwmarkToForward(chainFwFilter)
|
||||||
|
|
||||||
if err := m.rConn.Flush(); err != nil {
|
if err := m.rConn.Flush(); err != nil {
|
||||||
@@ -432,43 +480,6 @@ func (m *AclManager) allowRedirectedTraffic(chainFwFilter *nftables.Chain) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *AclManager) addPreroutingRule(preroutingChain *nftables.Chain) {
|
|
||||||
m.rConn.AddRule(&nftables.Rule{
|
|
||||||
Table: m.workTable,
|
|
||||||
Chain: preroutingChain,
|
|
||||||
Exprs: []expr.Any{
|
|
||||||
&expr.Meta{
|
|
||||||
Key: expr.MetaKeyIIFNAME,
|
|
||||||
Register: 1,
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: ifname(m.wgIface.Name()),
|
|
||||||
},
|
|
||||||
&expr.Fib{
|
|
||||||
Register: 1,
|
|
||||||
ResultADDRTYPE: true,
|
|
||||||
FlagDADDR: true,
|
|
||||||
},
|
|
||||||
&expr.Cmp{
|
|
||||||
Op: expr.CmpOpEq,
|
|
||||||
Register: 1,
|
|
||||||
Data: binaryutil.NativeEndian.PutUint32(unix.RTN_LOCAL),
|
|
||||||
},
|
|
||||||
&expr.Immediate{
|
|
||||||
Register: 1,
|
|
||||||
Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkRedirected),
|
|
||||||
},
|
|
||||||
&expr.Meta{
|
|
||||||
Key: expr.MetaKeyMARK,
|
|
||||||
Register: 1,
|
|
||||||
SourceRegister: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *AclManager) addFwmarkToForward(chainFwFilter *nftables.Chain) {
|
func (m *AclManager) addFwmarkToForward(chainFwFilter *nftables.Chain) {
|
||||||
m.rConn.InsertRule(&nftables.Rule{
|
m.rConn.InsertRule(&nftables.Rule{
|
||||||
Table: m.workTable,
|
Table: m.workTable,
|
||||||
@@ -484,8 +495,7 @@ func (m *AclManager) addFwmarkToForward(chainFwFilter *nftables.Chain) {
|
|||||||
Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkRedirected),
|
Data: binaryutil.NativeEndian.PutUint32(nbnet.PreroutingFwmarkRedirected),
|
||||||
},
|
},
|
||||||
&expr.Verdict{
|
&expr.Verdict{
|
||||||
Kind: expr.VerdictJump,
|
Kind: expr.VerdictAccept,
|
||||||
Chain: m.chainInputRules.Name,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -632,6 +642,7 @@ func (m *AclManager) flushWithBackoff() (err error) {
|
|||||||
for i := 0; ; i++ {
|
for i := 0; ; i++ {
|
||||||
err = m.rConn.Flush()
|
err = m.rConn.Flush()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Debugf("failed to flush nftables: %v", err)
|
||||||
if !strings.Contains(err.Error(), "busy") {
|
if !strings.Contains(err.Error(), "busy") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -648,7 +659,7 @@ func (m *AclManager) flushWithBackoff() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *AclManager) refreshRuleHandles(chain *nftables.Chain) error {
|
func (m *AclManager) refreshRuleHandles(chain *nftables.Chain, mangle bool) error {
|
||||||
if m.workTable == nil || chain == nil {
|
if m.workTable == nil || chain == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -665,9 +676,13 @@ func (m *AclManager) refreshRuleHandles(chain *nftables.Chain) error {
|
|||||||
split := bytes.Split(rule.UserData, []byte(" "))
|
split := bytes.Split(rule.UserData, []byte(" "))
|
||||||
r, ok := m.rules[string(split[0])]
|
r, ok := m.rules[string(split[0])]
|
||||||
if ok {
|
if ok {
|
||||||
|
if mangle {
|
||||||
|
*r.mangleRule = *rule
|
||||||
|
} else {
|
||||||
*r.nftRule = *rule
|
*r.nftRule = *rule
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -689,12 +704,6 @@ func generatePeerRuleId(ip net.IP, sPort *firewall.Port, dPort *firewall.Port, a
|
|||||||
return "set:" + ipset.Name + rulesetID
|
return "set:" + ipset.Name + rulesetID
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodePort(port firewall.Port) []byte {
|
|
||||||
bs := make([]byte, 2)
|
|
||||||
binary.BigEndian.PutUint16(bs, uint16(port.Values[0]))
|
|
||||||
return bs
|
|
||||||
}
|
|
||||||
|
|
||||||
func ifname(n string) []byte {
|
func ifname(n string) []byte {
|
||||||
b := make([]byte, 16)
|
b := make([]byte, 16)
|
||||||
copy(b, n+"\x00")
|
copy(b, n+"\x00")
|
||||||
|
|||||||
@@ -318,6 +318,19 @@ func (m *Manager) cleanupNetbirdTables() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLogLevel sets the log level for the firewall manager
|
||||||
|
func (m *Manager) SetLogLevel(log.Level) {
|
||||||
|
// not supported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) EnableRouting() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) DisableRouting() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Flush rule/chain/set operations from the buffer
|
// Flush rule/chain/set operations from the buffer
|
||||||
//
|
//
|
||||||
// Method also get all rules after flush and refreshes handle values in the rulesets
|
// Method also get all rules after flush and refreshes handle values in the rulesets
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func TestNftablesManager(t *testing.T) {
|
|||||||
|
|
||||||
testClient := &nftables.Conn{}
|
testClient := &nftables.Conn{}
|
||||||
|
|
||||||
rule, err := manager.AddPeerFiltering(ip, fw.ProtocolTCP, nil, &fw.Port{Values: []int{53}}, fw.ActionDrop, "", "")
|
rule, err := manager.AddPeerFiltering(ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{53}}, fw.ActionDrop, "", "")
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
|
|
||||||
err = manager.Flush()
|
err = manager.Flush()
|
||||||
@@ -107,7 +107,7 @@ func TestNftablesManager(t *testing.T) {
|
|||||||
Kind: expr.VerdictAccept,
|
Kind: expr.VerdictAccept,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
require.ElementsMatch(t, rules[0].Exprs, expectedExprs1, "expected the same expressions")
|
compareExprsIgnoringCounters(t, rules[0].Exprs, expectedExprs1)
|
||||||
|
|
||||||
ipToAdd, _ := netip.AddrFromSlice(ip)
|
ipToAdd, _ := netip.AddrFromSlice(ip)
|
||||||
add := ipToAdd.Unmap()
|
add := ipToAdd.Unmap()
|
||||||
@@ -200,7 +200,7 @@ func TestNFtablesCreatePerformance(t *testing.T) {
|
|||||||
ip := net.ParseIP("10.20.0.100")
|
ip := net.ParseIP("10.20.0.100")
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
for i := 0; i < testMax; i++ {
|
for i := 0; i < testMax; i++ {
|
||||||
port := &fw.Port{Values: []int{1000 + i}}
|
port := &fw.Port{Values: []uint16{uint16(1000 + i)}}
|
||||||
_, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic")
|
_, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic")
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
|
|
||||||
@@ -283,7 +283,7 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ip := net.ParseIP("100.96.0.1")
|
ip := net.ParseIP("100.96.0.1")
|
||||||
_, err = manager.AddPeerFiltering(ip, fw.ProtocolTCP, nil, &fw.Port{Values: []int{80}}, fw.ActionAccept, "", "test rule")
|
_, err = manager.AddPeerFiltering(ip, fw.ProtocolTCP, nil, &fw.Port{Values: []uint16{80}}, fw.ActionAccept, "", "test rule")
|
||||||
require.NoError(t, err, "failed to add peer filtering rule")
|
require.NoError(t, err, "failed to add peer filtering rule")
|
||||||
|
|
||||||
_, err = manager.AddRouteFiltering(
|
_, err = manager.AddRouteFiltering(
|
||||||
@@ -291,7 +291,7 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) {
|
|||||||
netip.MustParsePrefix("10.1.0.0/24"),
|
netip.MustParsePrefix("10.1.0.0/24"),
|
||||||
fw.ProtocolTCP,
|
fw.ProtocolTCP,
|
||||||
nil,
|
nil,
|
||||||
&fw.Port{Values: []int{443}},
|
&fw.Port{Values: []uint16{443}},
|
||||||
fw.ActionAccept,
|
fw.ActionAccept,
|
||||||
)
|
)
|
||||||
require.NoError(t, err, "failed to add route filtering rule")
|
require.NoError(t, err, "failed to add route filtering rule")
|
||||||
@@ -307,3 +307,18 @@ func TestNftablesManagerCompatibilityWithIptables(t *testing.T) {
|
|||||||
stdout, stderr = runIptablesSave(t)
|
stdout, stderr = runIptablesSave(t)
|
||||||
verifyIptablesOutput(t, stdout, stderr)
|
verifyIptablesOutput(t, stdout, stderr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func compareExprsIgnoringCounters(t *testing.T, got, want []expr.Any) {
|
||||||
|
t.Helper()
|
||||||
|
require.Equal(t, len(got), len(want), "expression count mismatch")
|
||||||
|
|
||||||
|
for i := range got {
|
||||||
|
if _, isCounter := got[i].(*expr.Counter); isCounter {
|
||||||
|
_, wantIsCounter := want[i].(*expr.Counter)
|
||||||
|
require.True(t, wantIsCounter, "expected Counter at index %d", i)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, got[i], want[i], "expression mismatch at index %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -233,7 +233,13 @@ func (r *router) AddRouteFiltering(
|
|||||||
UserData: []byte(ruleKey),
|
UserData: []byte(ruleKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insert DROP rules at the beginning, append ACCEPT rules at the end
|
||||||
|
if action == firewall.ActionDrop {
|
||||||
|
// TODO: Insert after the established rule
|
||||||
|
rule = r.conn.InsertRule(rule)
|
||||||
|
} else {
|
||||||
rule = r.conn.AddRule(rule)
|
rule = r.conn.AddRule(rule)
|
||||||
|
}
|
||||||
|
|
||||||
log.Tracef("Adding route rule %s", spew.Sdump(rule))
|
log.Tracef("Adding route rule %s", spew.Sdump(rule))
|
||||||
if err := r.conn.Flush(); err != nil {
|
if err := r.conn.Flush(); err != nil {
|
||||||
@@ -956,12 +962,12 @@ func applyPort(port *firewall.Port, isSource bool) []expr.Any {
|
|||||||
&expr.Cmp{
|
&expr.Cmp{
|
||||||
Op: expr.CmpOpGte,
|
Op: expr.CmpOpGte,
|
||||||
Register: 1,
|
Register: 1,
|
||||||
Data: binaryutil.BigEndian.PutUint16(uint16(port.Values[0])),
|
Data: binaryutil.BigEndian.PutUint16(port.Values[0]),
|
||||||
},
|
},
|
||||||
&expr.Cmp{
|
&expr.Cmp{
|
||||||
Op: expr.CmpOpLte,
|
Op: expr.CmpOpLte,
|
||||||
Register: 1,
|
Register: 1,
|
||||||
Data: binaryutil.BigEndian.PutUint16(uint16(port.Values[1])),
|
Data: binaryutil.BigEndian.PutUint16(port.Values[1]),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -980,7 +986,7 @@ func applyPort(port *firewall.Port, isSource bool) []expr.Any {
|
|||||||
exprs = append(exprs, &expr.Cmp{
|
exprs = append(exprs, &expr.Cmp{
|
||||||
Op: expr.CmpOpEq,
|
Op: expr.CmpOpEq,
|
||||||
Register: 1,
|
Register: 1,
|
||||||
Data: binaryutil.BigEndian.PutUint16(uint16(p)),
|
Data: binaryutil.BigEndian.PutUint16(p),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
destination: netip.MustParsePrefix("10.0.0.0/24"),
|
destination: netip.MustParsePrefix("10.0.0.0/24"),
|
||||||
proto: firewall.ProtocolTCP,
|
proto: firewall.ProtocolTCP,
|
||||||
sPort: nil,
|
sPort: nil,
|
||||||
dPort: &firewall.Port{Values: []int{80}},
|
dPort: &firewall.Port{Values: []uint16{80}},
|
||||||
direction: firewall.RuleDirectionIN,
|
direction: firewall.RuleDirectionIN,
|
||||||
action: firewall.ActionAccept,
|
action: firewall.ActionAccept,
|
||||||
expectSet: false,
|
expectSet: false,
|
||||||
@@ -235,7 +235,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
},
|
},
|
||||||
destination: netip.MustParsePrefix("10.0.0.0/8"),
|
destination: netip.MustParsePrefix("10.0.0.0/8"),
|
||||||
proto: firewall.ProtocolUDP,
|
proto: firewall.ProtocolUDP,
|
||||||
sPort: &firewall.Port{Values: []int{1024, 2048}, IsRange: true},
|
sPort: &firewall.Port{Values: []uint16{1024, 2048}, IsRange: true},
|
||||||
dPort: nil,
|
dPort: nil,
|
||||||
direction: firewall.RuleDirectionOUT,
|
direction: firewall.RuleDirectionOUT,
|
||||||
action: firewall.ActionDrop,
|
action: firewall.ActionDrop,
|
||||||
@@ -268,7 +268,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
sources: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/12")},
|
sources: []netip.Prefix{netip.MustParsePrefix("172.16.0.0/12")},
|
||||||
destination: netip.MustParsePrefix("192.168.0.0/16"),
|
destination: netip.MustParsePrefix("192.168.0.0/16"),
|
||||||
proto: firewall.ProtocolTCP,
|
proto: firewall.ProtocolTCP,
|
||||||
sPort: &firewall.Port{Values: []int{80, 443, 8080}},
|
sPort: &firewall.Port{Values: []uint16{80, 443, 8080}},
|
||||||
dPort: nil,
|
dPort: nil,
|
||||||
direction: firewall.RuleDirectionOUT,
|
direction: firewall.RuleDirectionOUT,
|
||||||
action: firewall.ActionAccept,
|
action: firewall.ActionAccept,
|
||||||
@@ -280,7 +280,7 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
destination: netip.MustParsePrefix("10.0.0.0/24"),
|
destination: netip.MustParsePrefix("10.0.0.0/24"),
|
||||||
proto: firewall.ProtocolUDP,
|
proto: firewall.ProtocolUDP,
|
||||||
sPort: nil,
|
sPort: nil,
|
||||||
dPort: &firewall.Port{Values: []int{5000, 5100}, IsRange: true},
|
dPort: &firewall.Port{Values: []uint16{5000, 5100}, IsRange: true},
|
||||||
direction: firewall.RuleDirectionIN,
|
direction: firewall.RuleDirectionIN,
|
||||||
action: firewall.ActionDrop,
|
action: firewall.ActionDrop,
|
||||||
expectSet: false,
|
expectSet: false,
|
||||||
@@ -290,8 +290,8 @@ func TestRouter_AddRouteFiltering(t *testing.T) {
|
|||||||
sources: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")},
|
sources: []netip.Prefix{netip.MustParsePrefix("10.0.0.0/24")},
|
||||||
destination: netip.MustParsePrefix("172.16.0.0/16"),
|
destination: netip.MustParsePrefix("172.16.0.0/16"),
|
||||||
proto: firewall.ProtocolTCP,
|
proto: firewall.ProtocolTCP,
|
||||||
sPort: &firewall.Port{Values: []int{1024, 65535}, IsRange: true},
|
sPort: &firewall.Port{Values: []uint16{1024, 65535}, IsRange: true},
|
||||||
dPort: &firewall.Port{Values: []int{22}},
|
dPort: &firewall.Port{Values: []uint16{22}},
|
||||||
direction: firewall.RuleDirectionOUT,
|
direction: firewall.RuleDirectionOUT,
|
||||||
action: firewall.ActionAccept,
|
action: firewall.ActionAccept,
|
||||||
expectSet: false,
|
expectSet: false,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
// Rule to handle management of rules
|
// Rule to handle management of rules
|
||||||
type Rule struct {
|
type Rule struct {
|
||||||
nftRule *nftables.Rule
|
nftRule *nftables.Rule
|
||||||
|
mangleRule *nftables.Rule
|
||||||
nftSet *nftables.Set
|
nftSet *nftables.Set
|
||||||
ruleID string
|
ruleID string
|
||||||
ip net.IP
|
ip net.IP
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
package uspfilter
|
package uspfilter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
)
|
)
|
||||||
@@ -17,17 +22,29 @@ func (m *Manager) Reset(stateManager *statemanager.Manager) error {
|
|||||||
|
|
||||||
if m.udpTracker != nil {
|
if m.udpTracker != nil {
|
||||||
m.udpTracker.Close()
|
m.udpTracker.Close()
|
||||||
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout)
|
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.icmpTracker != nil {
|
if m.icmpTracker != nil {
|
||||||
m.icmpTracker.Close()
|
m.icmpTracker.Close()
|
||||||
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout)
|
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.tcpTracker != nil {
|
if m.tcpTracker != nil {
|
||||||
m.tcpTracker.Close()
|
m.tcpTracker.Close()
|
||||||
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout)
|
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.forwarder != nil {
|
||||||
|
m.forwarder.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.logger != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := m.logger.Stop(ctx); err != nil {
|
||||||
|
log.Errorf("failed to shutdown logger: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.nativeFirewall != nil {
|
if m.nativeFirewall != nil {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package uspfilter
|
package uspfilter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
@@ -29,17 +31,29 @@ func (m *Manager) Reset(*statemanager.Manager) error {
|
|||||||
|
|
||||||
if m.udpTracker != nil {
|
if m.udpTracker != nil {
|
||||||
m.udpTracker.Close()
|
m.udpTracker.Close()
|
||||||
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout)
|
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.icmpTracker != nil {
|
if m.icmpTracker != nil {
|
||||||
m.icmpTracker.Close()
|
m.icmpTracker.Close()
|
||||||
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout)
|
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.tcpTracker != nil {
|
if m.tcpTracker != nil {
|
||||||
m.tcpTracker.Close()
|
m.tcpTracker.Close()
|
||||||
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout)
|
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.forwarder != nil {
|
||||||
|
m.forwarder.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.logger != nil {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := m.logger.Stop(ctx); err != nil {
|
||||||
|
log.Errorf("failed to shutdown logger: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isWindowsFirewallReachable() {
|
if !isWindowsFirewallReachable() {
|
||||||
|
|||||||
16
client/firewall/uspfilter/common/iface.go
Normal file
16
client/firewall/uspfilter/common/iface.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IFaceMapper defines subset methods of interface required for manager
|
||||||
|
type IFaceMapper interface {
|
||||||
|
SetFilter(device.PacketFilter) error
|
||||||
|
Address() iface.WGAddress
|
||||||
|
GetWGDevice() *wgdevice.Device
|
||||||
|
GetDevice() *device.FilteredDevice
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@ type BaseConnTrack struct {
|
|||||||
SourcePort uint16
|
SourcePort uint16
|
||||||
DestPort uint16
|
DestPort uint16
|
||||||
lastSeen atomic.Int64 // Unix nano for atomic access
|
lastSeen atomic.Int64 // Unix nano for atomic access
|
||||||
established atomic.Bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// these small methods will be inlined by the compiler
|
// these small methods will be inlined by the compiler
|
||||||
@@ -25,16 +24,6 @@ func (b *BaseConnTrack) UpdateLastSeen() {
|
|||||||
b.lastSeen.Store(time.Now().UnixNano())
|
b.lastSeen.Store(time.Now().UnixNano())
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEstablished safely checks if connection is established
|
|
||||||
func (b *BaseConnTrack) IsEstablished() bool {
|
|
||||||
return b.established.Load()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetEstablished safely sets the established state
|
|
||||||
func (b *BaseConnTrack) SetEstablished(state bool) {
|
|
||||||
b.established.Store(state)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLastSeen safely gets the last seen timestamp
|
// GetLastSeen safely gets the last seen timestamp
|
||||||
func (b *BaseConnTrack) GetLastSeen() time.Time {
|
func (b *BaseConnTrack) GetLastSeen() time.Time {
|
||||||
return time.Unix(0, b.lastSeen.Load())
|
return time.Unix(0, b.lastSeen.Load())
|
||||||
|
|||||||
@@ -3,8 +3,14 @@ package conntrack
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var logger = log.NewFromLogrus(logrus.StandardLogger())
|
||||||
|
|
||||||
func BenchmarkIPOperations(b *testing.B) {
|
func BenchmarkIPOperations(b *testing.B) {
|
||||||
b.Run("MakeIPAddr", func(b *testing.B) {
|
b.Run("MakeIPAddr", func(b *testing.B) {
|
||||||
ip := net.ParseIP("192.168.1.1")
|
ip := net.ParseIP("192.168.1.1")
|
||||||
@@ -34,37 +40,11 @@ func BenchmarkIPOperations(b *testing.B) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
func BenchmarkAtomicOperations(b *testing.B) {
|
|
||||||
conn := &BaseConnTrack{}
|
|
||||||
b.Run("UpdateLastSeen", func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
conn.UpdateLastSeen()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("IsEstablished", func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = conn.IsEstablished()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("SetEstablished", func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
conn.SetEstablished(i%2 == 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
b.Run("GetLastSeen", func(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_ = conn.GetLastSeen()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Memory pressure tests
|
// Memory pressure tests
|
||||||
func BenchmarkMemoryPressure(b *testing.B) {
|
func BenchmarkMemoryPressure(b *testing.B) {
|
||||||
b.Run("TCPHighLoad", func(b *testing.B) {
|
b.Run("TCPHighLoad", func(b *testing.B) {
|
||||||
tracker := NewTCPTracker(DefaultTCPTimeout)
|
tracker := NewTCPTracker(DefaultTCPTimeout, logger)
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
// Generate different IPs
|
// Generate different IPs
|
||||||
@@ -89,7 +69,7 @@ func BenchmarkMemoryPressure(b *testing.B) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
b.Run("UDPHighLoad", func(b *testing.B) {
|
b.Run("UDPHighLoad", func(b *testing.B) {
|
||||||
tracker := NewUDPTracker(DefaultUDPTimeout)
|
tracker := NewUDPTracker(DefaultUDPTimeout, logger)
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
// Generate different IPs
|
// Generate different IPs
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
|
|
||||||
|
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -33,6 +35,7 @@ type ICMPConnTrack struct {
|
|||||||
|
|
||||||
// ICMPTracker manages ICMP connection states
|
// ICMPTracker manages ICMP connection states
|
||||||
type ICMPTracker struct {
|
type ICMPTracker struct {
|
||||||
|
logger *nblog.Logger
|
||||||
connections map[ICMPConnKey]*ICMPConnTrack
|
connections map[ICMPConnKey]*ICMPConnTrack
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
cleanupTicker *time.Ticker
|
cleanupTicker *time.Ticker
|
||||||
@@ -42,12 +45,13 @@ type ICMPTracker struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewICMPTracker creates a new ICMP connection tracker
|
// NewICMPTracker creates a new ICMP connection tracker
|
||||||
func NewICMPTracker(timeout time.Duration) *ICMPTracker {
|
func NewICMPTracker(timeout time.Duration, logger *nblog.Logger) *ICMPTracker {
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
timeout = DefaultICMPTimeout
|
timeout = DefaultICMPTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
tracker := &ICMPTracker{
|
tracker := &ICMPTracker{
|
||||||
|
logger: logger,
|
||||||
connections: make(map[ICMPConnKey]*ICMPConnTrack),
|
connections: make(map[ICMPConnKey]*ICMPConnTrack),
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
cleanupTicker: time.NewTicker(ICMPCleanupInterval),
|
cleanupTicker: time.NewTicker(ICMPCleanupInterval),
|
||||||
@@ -62,7 +66,6 @@ func NewICMPTracker(timeout time.Duration) *ICMPTracker {
|
|||||||
// TrackOutbound records an outbound ICMP Echo Request
|
// TrackOutbound records an outbound ICMP Echo Request
|
||||||
func (t *ICMPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, id uint16, seq uint16) {
|
func (t *ICMPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, id uint16, seq uint16) {
|
||||||
key := makeICMPKey(srcIP, dstIP, id, seq)
|
key := makeICMPKey(srcIP, dstIP, id, seq)
|
||||||
now := time.Now().UnixNano()
|
|
||||||
|
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
conn, exists := t.connections[key]
|
conn, exists := t.connections[key]
|
||||||
@@ -80,24 +83,19 @@ func (t *ICMPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, id uint16, seq u
|
|||||||
ID: id,
|
ID: id,
|
||||||
Sequence: seq,
|
Sequence: seq,
|
||||||
}
|
}
|
||||||
conn.lastSeen.Store(now)
|
conn.UpdateLastSeen()
|
||||||
conn.established.Store(true)
|
|
||||||
t.connections[key] = conn
|
t.connections[key] = conn
|
||||||
|
|
||||||
|
t.logger.Trace("New ICMP connection %v", key)
|
||||||
}
|
}
|
||||||
t.mutex.Unlock()
|
t.mutex.Unlock()
|
||||||
|
|
||||||
conn.lastSeen.Store(now)
|
conn.UpdateLastSeen()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValidInbound checks if an inbound ICMP Echo Reply matches a tracked request
|
// IsValidInbound checks if an inbound ICMP Echo Reply matches a tracked request
|
||||||
func (t *ICMPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, id uint16, seq uint16, icmpType uint8) bool {
|
func (t *ICMPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, id uint16, seq uint16, icmpType uint8) bool {
|
||||||
switch icmpType {
|
if icmpType != uint8(layers.ICMPv4TypeEchoReply) {
|
||||||
case uint8(layers.ICMPv4TypeDestinationUnreachable),
|
|
||||||
uint8(layers.ICMPv4TypeTimeExceeded):
|
|
||||||
return true
|
|
||||||
case uint8(layers.ICMPv4TypeEchoReply):
|
|
||||||
// continue processing
|
|
||||||
default:
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,8 +113,7 @@ func (t *ICMPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, id uint16, seq
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn.IsEstablished() &&
|
return ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) &&
|
||||||
ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) &&
|
|
||||||
ValidateIPs(MakeIPAddr(dstIP), conn.SourceIP) &&
|
ValidateIPs(MakeIPAddr(dstIP), conn.SourceIP) &&
|
||||||
conn.ID == id &&
|
conn.ID == id &&
|
||||||
conn.Sequence == seq
|
conn.Sequence == seq
|
||||||
@@ -141,6 +138,8 @@ func (t *ICMPTracker) cleanup() {
|
|||||||
t.ipPool.Put(conn.SourceIP)
|
t.ipPool.Put(conn.SourceIP)
|
||||||
t.ipPool.Put(conn.DestIP)
|
t.ipPool.Put(conn.DestIP)
|
||||||
delete(t.connections, key)
|
delete(t.connections, key)
|
||||||
|
|
||||||
|
t.logger.Debug("Removed ICMP connection %v (timeout)", key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
func BenchmarkICMPTracker(b *testing.B) {
|
func BenchmarkICMPTracker(b *testing.B) {
|
||||||
b.Run("TrackOutbound", func(b *testing.B) {
|
b.Run("TrackOutbound", func(b *testing.B) {
|
||||||
tracker := NewICMPTracker(DefaultICMPTimeout)
|
tracker := NewICMPTracker(DefaultICMPTimeout, logger)
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
srcIP := net.ParseIP("192.168.1.1")
|
srcIP := net.ParseIP("192.168.1.1")
|
||||||
@@ -20,7 +20,7 @@ func BenchmarkICMPTracker(b *testing.B) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
b.Run("IsValidInbound", func(b *testing.B) {
|
b.Run("IsValidInbound", func(b *testing.B) {
|
||||||
tracker := NewICMPTracker(DefaultICMPTimeout)
|
tracker := NewICMPTracker(DefaultICMPTimeout, logger)
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
srcIP := net.ParseIP("192.168.1.1")
|
srcIP := net.ParseIP("192.168.1.1")
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ package conntrack
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -62,11 +65,23 @@ type TCPConnKey struct {
|
|||||||
type TCPConnTrack struct {
|
type TCPConnTrack struct {
|
||||||
BaseConnTrack
|
BaseConnTrack
|
||||||
State TCPState
|
State TCPState
|
||||||
|
established atomic.Bool
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsEstablished safely checks if connection is established
|
||||||
|
func (t *TCPConnTrack) IsEstablished() bool {
|
||||||
|
return t.established.Load()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEstablished safely sets the established state
|
||||||
|
func (t *TCPConnTrack) SetEstablished(state bool) {
|
||||||
|
t.established.Store(state)
|
||||||
|
}
|
||||||
|
|
||||||
// TCPTracker manages TCP connection states
|
// TCPTracker manages TCP connection states
|
||||||
type TCPTracker struct {
|
type TCPTracker struct {
|
||||||
|
logger *nblog.Logger
|
||||||
connections map[ConnKey]*TCPConnTrack
|
connections map[ConnKey]*TCPConnTrack
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
cleanupTicker *time.Ticker
|
cleanupTicker *time.Ticker
|
||||||
@@ -76,8 +91,9 @@ type TCPTracker struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewTCPTracker creates a new TCP connection tracker
|
// NewTCPTracker creates a new TCP connection tracker
|
||||||
func NewTCPTracker(timeout time.Duration) *TCPTracker {
|
func NewTCPTracker(timeout time.Duration, logger *nblog.Logger) *TCPTracker {
|
||||||
tracker := &TCPTracker{
|
tracker := &TCPTracker{
|
||||||
|
logger: logger,
|
||||||
connections: make(map[ConnKey]*TCPConnTrack),
|
connections: make(map[ConnKey]*TCPConnTrack),
|
||||||
cleanupTicker: time.NewTicker(TCPCleanupInterval),
|
cleanupTicker: time.NewTicker(TCPCleanupInterval),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
@@ -93,7 +109,6 @@ func NewTCPTracker(timeout time.Duration) *TCPTracker {
|
|||||||
func (t *TCPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16, flags uint8) {
|
func (t *TCPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16, flags uint8) {
|
||||||
// Create key before lock
|
// Create key before lock
|
||||||
key := makeConnKey(srcIP, dstIP, srcPort, dstPort)
|
key := makeConnKey(srcIP, dstIP, srcPort, dstPort)
|
||||||
now := time.Now().UnixNano()
|
|
||||||
|
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
conn, exists := t.connections[key]
|
conn, exists := t.connections[key]
|
||||||
@@ -113,9 +128,11 @@ func (t *TCPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, d
|
|||||||
},
|
},
|
||||||
State: TCPStateNew,
|
State: TCPStateNew,
|
||||||
}
|
}
|
||||||
conn.lastSeen.Store(now)
|
conn.UpdateLastSeen()
|
||||||
conn.established.Store(false)
|
conn.established.Store(false)
|
||||||
t.connections[key] = conn
|
t.connections[key] = conn
|
||||||
|
|
||||||
|
t.logger.Trace("New TCP connection: %s:%d -> %s:%d", srcIP, srcPort, dstIP, dstPort)
|
||||||
}
|
}
|
||||||
t.mutex.Unlock()
|
t.mutex.Unlock()
|
||||||
|
|
||||||
@@ -123,7 +140,7 @@ func (t *TCPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, d
|
|||||||
conn.Lock()
|
conn.Lock()
|
||||||
t.updateState(conn, flags, true)
|
t.updateState(conn, flags, true)
|
||||||
conn.Unlock()
|
conn.Unlock()
|
||||||
conn.lastSeen.Store(now)
|
conn.UpdateLastSeen()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValidInbound checks if an inbound TCP packet matches a tracked connection
|
// IsValidInbound checks if an inbound TCP packet matches a tracked connection
|
||||||
@@ -171,6 +188,9 @@ func (t *TCPTracker) updateState(conn *TCPConnTrack, flags uint8, isOutbound boo
|
|||||||
if flags&TCPRst != 0 {
|
if flags&TCPRst != 0 {
|
||||||
conn.State = TCPStateClosed
|
conn.State = TCPStateClosed
|
||||||
conn.SetEstablished(false)
|
conn.SetEstablished(false)
|
||||||
|
|
||||||
|
t.logger.Trace("TCP connection reset: %s:%d -> %s:%d",
|
||||||
|
conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +247,9 @@ func (t *TCPTracker) updateState(conn *TCPConnTrack, flags uint8, isOutbound boo
|
|||||||
if flags&TCPAck != 0 {
|
if flags&TCPAck != 0 {
|
||||||
conn.State = TCPStateTimeWait
|
conn.State = TCPStateTimeWait
|
||||||
// Keep established = false from previous state
|
// Keep established = false from previous state
|
||||||
|
|
||||||
|
t.logger.Trace("TCP connection closed (simultaneous) - %s:%d -> %s:%d",
|
||||||
|
conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
case TCPStateCloseWait:
|
case TCPStateCloseWait:
|
||||||
@@ -237,11 +260,17 @@ func (t *TCPTracker) updateState(conn *TCPConnTrack, flags uint8, isOutbound boo
|
|||||||
case TCPStateLastAck:
|
case TCPStateLastAck:
|
||||||
if flags&TCPAck != 0 {
|
if flags&TCPAck != 0 {
|
||||||
conn.State = TCPStateClosed
|
conn.State = TCPStateClosed
|
||||||
|
|
||||||
|
t.logger.Trace("TCP connection gracefully closed: %s:%d -> %s:%d",
|
||||||
|
conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
case TCPStateTimeWait:
|
case TCPStateTimeWait:
|
||||||
// Stay in TIME-WAIT for 2MSL before transitioning to closed
|
// Stay in TIME-WAIT for 2MSL before transitioning to closed
|
||||||
// This is handled by the cleanup routine
|
// This is handled by the cleanup routine
|
||||||
|
|
||||||
|
t.logger.Trace("TCP connection completed - %s:%d -> %s:%d",
|
||||||
|
conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,6 +347,8 @@ func (t *TCPTracker) cleanup() {
|
|||||||
t.ipPool.Put(conn.SourceIP)
|
t.ipPool.Put(conn.SourceIP)
|
||||||
t.ipPool.Put(conn.DestIP)
|
t.ipPool.Put(conn.DestIP)
|
||||||
delete(t.connections, key)
|
delete(t.connections, key)
|
||||||
|
|
||||||
|
t.logger.Trace("Cleaned up TCP connection: %s:%d -> %s:%d", conn.SourceIP, conn.SourcePort, conn.DestIP, conn.DestPort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestTCPStateMachine(t *testing.T) {
|
func TestTCPStateMachine(t *testing.T) {
|
||||||
tracker := NewTCPTracker(DefaultTCPTimeout)
|
tracker := NewTCPTracker(DefaultTCPTimeout, logger)
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
srcIP := net.ParseIP("100.64.0.1")
|
srcIP := net.ParseIP("100.64.0.1")
|
||||||
@@ -154,7 +154,7 @@ func TestTCPStateMachine(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
tracker = NewTCPTracker(DefaultTCPTimeout)
|
tracker = NewTCPTracker(DefaultTCPTimeout, logger)
|
||||||
tt.test(t)
|
tt.test(t)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@ func TestTCPStateMachine(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRSTHandling(t *testing.T) {
|
func TestRSTHandling(t *testing.T) {
|
||||||
tracker := NewTCPTracker(DefaultTCPTimeout)
|
tracker := NewTCPTracker(DefaultTCPTimeout, logger)
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
srcIP := net.ParseIP("100.64.0.1")
|
srcIP := net.ParseIP("100.64.0.1")
|
||||||
@@ -233,7 +233,7 @@ func establishConnection(t *testing.T, tracker *TCPTracker, srcIP, dstIP net.IP,
|
|||||||
|
|
||||||
func BenchmarkTCPTracker(b *testing.B) {
|
func BenchmarkTCPTracker(b *testing.B) {
|
||||||
b.Run("TrackOutbound", func(b *testing.B) {
|
b.Run("TrackOutbound", func(b *testing.B) {
|
||||||
tracker := NewTCPTracker(DefaultTCPTimeout)
|
tracker := NewTCPTracker(DefaultTCPTimeout, logger)
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
srcIP := net.ParseIP("192.168.1.1")
|
srcIP := net.ParseIP("192.168.1.1")
|
||||||
@@ -246,7 +246,7 @@ func BenchmarkTCPTracker(b *testing.B) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
b.Run("IsValidInbound", func(b *testing.B) {
|
b.Run("IsValidInbound", func(b *testing.B) {
|
||||||
tracker := NewTCPTracker(DefaultTCPTimeout)
|
tracker := NewTCPTracker(DefaultTCPTimeout, logger)
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
srcIP := net.ParseIP("192.168.1.1")
|
srcIP := net.ParseIP("192.168.1.1")
|
||||||
@@ -264,7 +264,7 @@ func BenchmarkTCPTracker(b *testing.B) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
b.Run("ConcurrentAccess", func(b *testing.B) {
|
b.Run("ConcurrentAccess", func(b *testing.B) {
|
||||||
tracker := NewTCPTracker(DefaultTCPTimeout)
|
tracker := NewTCPTracker(DefaultTCPTimeout, logger)
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
srcIP := net.ParseIP("192.168.1.1")
|
srcIP := net.ParseIP("192.168.1.1")
|
||||||
@@ -287,7 +287,7 @@ func BenchmarkTCPTracker(b *testing.B) {
|
|||||||
// Benchmark connection cleanup
|
// Benchmark connection cleanup
|
||||||
func BenchmarkCleanup(b *testing.B) {
|
func BenchmarkCleanup(b *testing.B) {
|
||||||
b.Run("TCPCleanup", func(b *testing.B) {
|
b.Run("TCPCleanup", func(b *testing.B) {
|
||||||
tracker := NewTCPTracker(100 * time.Millisecond) // Short timeout for testing
|
tracker := NewTCPTracker(100*time.Millisecond, logger) // Short timeout for testing
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
// Pre-populate with expired connections
|
// Pre-populate with expired connections
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -20,6 +22,7 @@ type UDPConnTrack struct {
|
|||||||
|
|
||||||
// UDPTracker manages UDP connection states
|
// UDPTracker manages UDP connection states
|
||||||
type UDPTracker struct {
|
type UDPTracker struct {
|
||||||
|
logger *nblog.Logger
|
||||||
connections map[ConnKey]*UDPConnTrack
|
connections map[ConnKey]*UDPConnTrack
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
cleanupTicker *time.Ticker
|
cleanupTicker *time.Ticker
|
||||||
@@ -29,12 +32,13 @@ type UDPTracker struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewUDPTracker creates a new UDP connection tracker
|
// NewUDPTracker creates a new UDP connection tracker
|
||||||
func NewUDPTracker(timeout time.Duration) *UDPTracker {
|
func NewUDPTracker(timeout time.Duration, logger *nblog.Logger) *UDPTracker {
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
timeout = DefaultUDPTimeout
|
timeout = DefaultUDPTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
tracker := &UDPTracker{
|
tracker := &UDPTracker{
|
||||||
|
logger: logger,
|
||||||
connections: make(map[ConnKey]*UDPConnTrack),
|
connections: make(map[ConnKey]*UDPConnTrack),
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
cleanupTicker: time.NewTicker(UDPCleanupInterval),
|
cleanupTicker: time.NewTicker(UDPCleanupInterval),
|
||||||
@@ -49,7 +53,6 @@ func NewUDPTracker(timeout time.Duration) *UDPTracker {
|
|||||||
// TrackOutbound records an outbound UDP connection
|
// TrackOutbound records an outbound UDP connection
|
||||||
func (t *UDPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16) {
|
func (t *UDPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, dstPort uint16) {
|
||||||
key := makeConnKey(srcIP, dstIP, srcPort, dstPort)
|
key := makeConnKey(srcIP, dstIP, srcPort, dstPort)
|
||||||
now := time.Now().UnixNano()
|
|
||||||
|
|
||||||
t.mutex.Lock()
|
t.mutex.Lock()
|
||||||
conn, exists := t.connections[key]
|
conn, exists := t.connections[key]
|
||||||
@@ -67,13 +70,14 @@ func (t *UDPTracker) TrackOutbound(srcIP net.IP, dstIP net.IP, srcPort uint16, d
|
|||||||
DestPort: dstPort,
|
DestPort: dstPort,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
conn.lastSeen.Store(now)
|
conn.UpdateLastSeen()
|
||||||
conn.established.Store(true)
|
|
||||||
t.connections[key] = conn
|
t.connections[key] = conn
|
||||||
|
|
||||||
|
t.logger.Trace("New UDP connection: %v", conn)
|
||||||
}
|
}
|
||||||
t.mutex.Unlock()
|
t.mutex.Unlock()
|
||||||
|
|
||||||
conn.lastSeen.Store(now)
|
conn.UpdateLastSeen()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValidInbound checks if an inbound packet matches a tracked connection
|
// IsValidInbound checks if an inbound packet matches a tracked connection
|
||||||
@@ -92,8 +96,7 @@ func (t *UDPTracker) IsValidInbound(srcIP net.IP, dstIP net.IP, srcPort uint16,
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return conn.IsEstablished() &&
|
return ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) &&
|
||||||
ValidateIPs(MakeIPAddr(srcIP), conn.DestIP) &&
|
|
||||||
ValidateIPs(MakeIPAddr(dstIP), conn.SourceIP) &&
|
ValidateIPs(MakeIPAddr(dstIP), conn.SourceIP) &&
|
||||||
conn.DestPort == srcPort &&
|
conn.DestPort == srcPort &&
|
||||||
conn.SourcePort == dstPort
|
conn.SourcePort == dstPort
|
||||||
@@ -120,6 +123,8 @@ func (t *UDPTracker) cleanup() {
|
|||||||
t.ipPool.Put(conn.SourceIP)
|
t.ipPool.Put(conn.SourceIP)
|
||||||
t.ipPool.Put(conn.DestIP)
|
t.ipPool.Put(conn.DestIP)
|
||||||
delete(t.connections, key)
|
delete(t.connections, key)
|
||||||
|
|
||||||
|
t.logger.Trace("Removed UDP connection %v (timeout)", conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ func TestNewUDPTracker(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
tracker := NewUDPTracker(tt.timeout)
|
tracker := NewUDPTracker(tt.timeout, logger)
|
||||||
assert.NotNil(t, tracker)
|
assert.NotNil(t, tracker)
|
||||||
assert.Equal(t, tt.wantTimeout, tracker.timeout)
|
assert.Equal(t, tt.wantTimeout, tracker.timeout)
|
||||||
assert.NotNil(t, tracker.connections)
|
assert.NotNil(t, tracker.connections)
|
||||||
@@ -40,7 +40,7 @@ func TestNewUDPTracker(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUDPTracker_TrackOutbound(t *testing.T) {
|
func TestUDPTracker_TrackOutbound(t *testing.T) {
|
||||||
tracker := NewUDPTracker(DefaultUDPTimeout)
|
tracker := NewUDPTracker(DefaultUDPTimeout, logger)
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
srcIP := net.ParseIP("192.168.1.2")
|
srcIP := net.ParseIP("192.168.1.2")
|
||||||
@@ -58,12 +58,11 @@ func TestUDPTracker_TrackOutbound(t *testing.T) {
|
|||||||
assert.True(t, conn.DestIP.Equal(dstIP))
|
assert.True(t, conn.DestIP.Equal(dstIP))
|
||||||
assert.Equal(t, srcPort, conn.SourcePort)
|
assert.Equal(t, srcPort, conn.SourcePort)
|
||||||
assert.Equal(t, dstPort, conn.DestPort)
|
assert.Equal(t, dstPort, conn.DestPort)
|
||||||
assert.True(t, conn.IsEstablished())
|
|
||||||
assert.WithinDuration(t, time.Now(), conn.GetLastSeen(), 1*time.Second)
|
assert.WithinDuration(t, time.Now(), conn.GetLastSeen(), 1*time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUDPTracker_IsValidInbound(t *testing.T) {
|
func TestUDPTracker_IsValidInbound(t *testing.T) {
|
||||||
tracker := NewUDPTracker(1 * time.Second)
|
tracker := NewUDPTracker(1*time.Second, logger)
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
srcIP := net.ParseIP("192.168.1.2")
|
srcIP := net.ParseIP("192.168.1.2")
|
||||||
@@ -162,6 +161,7 @@ func TestUDPTracker_Cleanup(t *testing.T) {
|
|||||||
cleanupTicker: time.NewTicker(cleanupInterval),
|
cleanupTicker: time.NewTicker(cleanupInterval),
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
ipPool: NewPreallocatedIPs(),
|
ipPool: NewPreallocatedIPs(),
|
||||||
|
logger: logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start cleanup routine
|
// Start cleanup routine
|
||||||
@@ -211,7 +211,7 @@ func TestUDPTracker_Cleanup(t *testing.T) {
|
|||||||
|
|
||||||
func BenchmarkUDPTracker(b *testing.B) {
|
func BenchmarkUDPTracker(b *testing.B) {
|
||||||
b.Run("TrackOutbound", func(b *testing.B) {
|
b.Run("TrackOutbound", func(b *testing.B) {
|
||||||
tracker := NewUDPTracker(DefaultUDPTimeout)
|
tracker := NewUDPTracker(DefaultUDPTimeout, logger)
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
srcIP := net.ParseIP("192.168.1.1")
|
srcIP := net.ParseIP("192.168.1.1")
|
||||||
@@ -224,7 +224,7 @@ func BenchmarkUDPTracker(b *testing.B) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
b.Run("IsValidInbound", func(b *testing.B) {
|
b.Run("IsValidInbound", func(b *testing.B) {
|
||||||
tracker := NewUDPTracker(DefaultUDPTimeout)
|
tracker := NewUDPTracker(DefaultUDPTimeout, logger)
|
||||||
defer tracker.Close()
|
defer tracker.Close()
|
||||||
|
|
||||||
srcIP := net.ParseIP("192.168.1.1")
|
srcIP := net.ParseIP("192.168.1.1")
|
||||||
|
|||||||
81
client/firewall/uspfilter/forwarder/endpoint.go
Normal file
81
client/firewall/uspfilter/forwarder/endpoint.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package forwarder
|
||||||
|
|
||||||
|
import (
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||||
|
|
||||||
|
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// endpoint implements stack.LinkEndpoint and handles integration with the wireguard device
|
||||||
|
type endpoint struct {
|
||||||
|
logger *nblog.Logger
|
||||||
|
dispatcher stack.NetworkDispatcher
|
||||||
|
device *wgdevice.Device
|
||||||
|
mtu uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *endpoint) Attach(dispatcher stack.NetworkDispatcher) {
|
||||||
|
e.dispatcher = dispatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *endpoint) IsAttached() bool {
|
||||||
|
return e.dispatcher != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *endpoint) MTU() uint32 {
|
||||||
|
return e.mtu
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *endpoint) Capabilities() stack.LinkEndpointCapabilities {
|
||||||
|
return stack.CapabilityNone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *endpoint) MaxHeaderLength() uint16 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *endpoint) LinkAddress() tcpip.LinkAddress {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *endpoint) WritePackets(pkts stack.PacketBufferList) (int, tcpip.Error) {
|
||||||
|
var written int
|
||||||
|
for _, pkt := range pkts.AsSlice() {
|
||||||
|
netHeader := header.IPv4(pkt.NetworkHeader().View().AsSlice())
|
||||||
|
|
||||||
|
data := stack.PayloadSince(pkt.NetworkHeader())
|
||||||
|
if data == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the packet through WireGuard
|
||||||
|
address := netHeader.DestinationAddress()
|
||||||
|
err := e.device.CreateOutboundPacket(data.AsSlice(), address.AsSlice())
|
||||||
|
if err != nil {
|
||||||
|
e.logger.Error("CreateOutboundPacket: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
written++
|
||||||
|
}
|
||||||
|
|
||||||
|
return written, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *endpoint) Wait() {
|
||||||
|
// not required
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *endpoint) ARPHardwareType() header.ARPHardwareType {
|
||||||
|
return header.ARPHardwareNone
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *endpoint) AddHeader(*stack.PacketBuffer) {
|
||||||
|
// not required
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *endpoint) ParseHeader(*stack.PacketBuffer) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
166
client/firewall/uspfilter/forwarder/forwarder.go
Normal file
166
client/firewall/uspfilter/forwarder/forwarder.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package forwarder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gvisor.dev/gvisor/pkg/buffer"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/common"
|
||||||
|
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultReceiveWindow = 32768
|
||||||
|
defaultMaxInFlight = 1024
|
||||||
|
iosReceiveWindow = 16384
|
||||||
|
iosMaxInFlight = 256
|
||||||
|
)
|
||||||
|
|
||||||
|
type Forwarder struct {
|
||||||
|
logger *nblog.Logger
|
||||||
|
stack *stack.Stack
|
||||||
|
endpoint *endpoint
|
||||||
|
udpForwarder *udpForwarder
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
ip net.IP
|
||||||
|
netstack bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(iface common.IFaceMapper, logger *nblog.Logger, netstack bool) (*Forwarder, error) {
|
||||||
|
s := stack.New(stack.Options{
|
||||||
|
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol},
|
||||||
|
TransportProtocols: []stack.TransportProtocolFactory{
|
||||||
|
tcp.NewProtocol,
|
||||||
|
udp.NewProtocol,
|
||||||
|
icmp.NewProtocol4,
|
||||||
|
},
|
||||||
|
HandleLocal: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
mtu, err := iface.GetDevice().MTU()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("get MTU: %w", err)
|
||||||
|
}
|
||||||
|
nicID := tcpip.NICID(1)
|
||||||
|
endpoint := &endpoint{
|
||||||
|
logger: logger,
|
||||||
|
device: iface.GetWGDevice(),
|
||||||
|
mtu: uint32(mtu),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.CreateNIC(nicID, endpoint); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create NIC: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ones, _ := iface.Address().Network.Mask.Size()
|
||||||
|
protoAddr := tcpip.ProtocolAddress{
|
||||||
|
Protocol: ipv4.ProtocolNumber,
|
||||||
|
AddressWithPrefix: tcpip.AddressWithPrefix{
|
||||||
|
Address: tcpip.AddrFromSlice(iface.Address().IP.To4()),
|
||||||
|
PrefixLen: ones,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.AddProtocolAddress(nicID, protoAddr, stack.AddressProperties{}); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to add protocol address: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultSubnet, err := tcpip.NewSubnet(
|
||||||
|
tcpip.AddrFrom4([4]byte{0, 0, 0, 0}),
|
||||||
|
tcpip.MaskFromBytes([]byte{0, 0, 0, 0}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("creating default subnet: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.SetPromiscuousMode(nicID, true); err != nil {
|
||||||
|
return nil, fmt.Errorf("set promiscuous mode: %s", err)
|
||||||
|
}
|
||||||
|
if err := s.SetSpoofing(nicID, true); err != nil {
|
||||||
|
return nil, fmt.Errorf("set spoofing: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.SetRouteTable([]tcpip.Route{
|
||||||
|
{
|
||||||
|
Destination: defaultSubnet,
|
||||||
|
NIC: nicID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
f := &Forwarder{
|
||||||
|
logger: logger,
|
||||||
|
stack: s,
|
||||||
|
endpoint: endpoint,
|
||||||
|
udpForwarder: newUDPForwarder(mtu, logger),
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
netstack: netstack,
|
||||||
|
ip: iface.Address().IP,
|
||||||
|
}
|
||||||
|
|
||||||
|
receiveWindow := defaultReceiveWindow
|
||||||
|
maxInFlight := defaultMaxInFlight
|
||||||
|
if runtime.GOOS == "ios" {
|
||||||
|
receiveWindow = iosReceiveWindow
|
||||||
|
maxInFlight = iosMaxInFlight
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpForwarder := tcp.NewForwarder(s, receiveWindow, maxInFlight, f.handleTCP)
|
||||||
|
s.SetTransportProtocolHandler(tcp.ProtocolNumber, tcpForwarder.HandlePacket)
|
||||||
|
|
||||||
|
udpForwarder := udp.NewForwarder(s, f.handleUDP)
|
||||||
|
s.SetTransportProtocolHandler(udp.ProtocolNumber, udpForwarder.HandlePacket)
|
||||||
|
|
||||||
|
s.SetTransportProtocolHandler(icmp.ProtocolNumber4, f.handleICMP)
|
||||||
|
|
||||||
|
log.Debugf("forwarder: Initialization complete with NIC %d", nicID)
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forwarder) InjectIncomingPacket(payload []byte) error {
|
||||||
|
if len(payload) < header.IPv4MinimumSize {
|
||||||
|
return fmt.Errorf("packet too small: %d bytes", len(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
pkt := stack.NewPacketBuffer(stack.PacketBufferOptions{
|
||||||
|
Payload: buffer.MakeWithData(payload),
|
||||||
|
})
|
||||||
|
defer pkt.DecRef()
|
||||||
|
|
||||||
|
if f.endpoint.dispatcher != nil {
|
||||||
|
f.endpoint.dispatcher.DeliverNetworkPacket(ipv4.ProtocolNumber, pkt)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop gracefully shuts down the forwarder
|
||||||
|
func (f *Forwarder) Stop() {
|
||||||
|
f.cancel()
|
||||||
|
|
||||||
|
if f.udpForwarder != nil {
|
||||||
|
f.udpForwarder.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
f.stack.Close()
|
||||||
|
f.stack.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forwarder) determineDialAddr(addr tcpip.Address) net.IP {
|
||||||
|
if f.netstack && f.ip.Equal(addr.AsSlice()) {
|
||||||
|
return net.IPv4(127, 0, 0, 1)
|
||||||
|
}
|
||||||
|
return addr.AsSlice()
|
||||||
|
}
|
||||||
109
client/firewall/uspfilter/forwarder/icmp.go
Normal file
109
client/firewall/uspfilter/forwarder/icmp.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package forwarder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/header"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleICMP handles ICMP packets from the network stack
|
||||||
|
func (f *Forwarder) handleICMP(id stack.TransportEndpointID, pkt stack.PacketBufferPtr) bool {
|
||||||
|
ctx, cancel := context.WithTimeout(f.ctx, 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
lc := net.ListenConfig{}
|
||||||
|
// TODO: support non-root
|
||||||
|
conn, err := lc.ListenPacket(ctx, "ip4:icmp", "0.0.0.0")
|
||||||
|
if err != nil {
|
||||||
|
f.logger.Error("Failed to create ICMP socket for %v: %v", id, err)
|
||||||
|
|
||||||
|
// This will make netstack reply on behalf of the original destination, that's ok for now
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
f.logger.Debug("Failed to close ICMP socket: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
dstIP := f.determineDialAddr(id.LocalAddress)
|
||||||
|
dst := &net.IPAddr{IP: dstIP}
|
||||||
|
|
||||||
|
// Get the complete ICMP message (header + data)
|
||||||
|
fullPacket := stack.PayloadSince(pkt.TransportHeader())
|
||||||
|
payload := fullPacket.AsSlice()
|
||||||
|
|
||||||
|
icmpHdr := header.ICMPv4(pkt.TransportHeader().View().AsSlice())
|
||||||
|
|
||||||
|
// For Echo Requests, send and handle response
|
||||||
|
switch icmpHdr.Type() {
|
||||||
|
case header.ICMPv4Echo:
|
||||||
|
return f.handleEchoResponse(icmpHdr, payload, dst, conn, id)
|
||||||
|
case header.ICMPv4EchoReply:
|
||||||
|
// dont process our own replies
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other ICMP types (Time Exceeded, Destination Unreachable, etc)
|
||||||
|
_, err = conn.WriteTo(payload, dst)
|
||||||
|
if err != nil {
|
||||||
|
f.logger.Error("Failed to write ICMP packet for %v: %v", id, err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
f.logger.Trace("Forwarded ICMP packet %v type=%v code=%v",
|
||||||
|
id, icmpHdr.Type(), icmpHdr.Code())
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forwarder) handleEchoResponse(icmpHdr header.ICMPv4, payload []byte, dst *net.IPAddr, conn net.PacketConn, id stack.TransportEndpointID) bool {
|
||||||
|
if _, err := conn.WriteTo(payload, dst); err != nil {
|
||||||
|
f.logger.Error("Failed to write ICMP packet for %v: %v", id, err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
f.logger.Trace("Forwarded ICMP packet %v type=%v code=%v",
|
||||||
|
id, icmpHdr.Type(), icmpHdr.Code())
|
||||||
|
|
||||||
|
if err := conn.SetReadDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||||||
|
f.logger.Error("Failed to set read deadline for ICMP response: %v", err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
response := make([]byte, f.endpoint.mtu)
|
||||||
|
n, _, err := conn.ReadFrom(response)
|
||||||
|
if err != nil {
|
||||||
|
if !isTimeout(err) {
|
||||||
|
f.logger.Error("Failed to read ICMP response: %v", err)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
ipHdr := make([]byte, header.IPv4MinimumSize)
|
||||||
|
ip := header.IPv4(ipHdr)
|
||||||
|
ip.Encode(&header.IPv4Fields{
|
||||||
|
TotalLength: uint16(header.IPv4MinimumSize + n),
|
||||||
|
TTL: 64,
|
||||||
|
Protocol: uint8(header.ICMPv4ProtocolNumber),
|
||||||
|
SrcAddr: id.LocalAddress,
|
||||||
|
DstAddr: id.RemoteAddress,
|
||||||
|
})
|
||||||
|
ip.SetChecksum(^ip.CalculateChecksum())
|
||||||
|
|
||||||
|
fullPacket := make([]byte, 0, len(ipHdr)+n)
|
||||||
|
fullPacket = append(fullPacket, ipHdr...)
|
||||||
|
fullPacket = append(fullPacket, response[:n]...)
|
||||||
|
|
||||||
|
if err := f.InjectIncomingPacket(fullPacket); err != nil {
|
||||||
|
f.logger.Error("Failed to inject ICMP response: %v", err)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
f.logger.Trace("Forwarded ICMP echo reply for %v", id)
|
||||||
|
return true
|
||||||
|
}
|
||||||
90
client/firewall/uspfilter/forwarder/tcp.go
Normal file
90
client/firewall/uspfilter/forwarder/tcp.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package forwarder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
|
||||||
|
"gvisor.dev/gvisor/pkg/waiter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// handleTCP is called by the TCP forwarder for new connections.
|
||||||
|
func (f *Forwarder) handleTCP(r *tcp.ForwarderRequest) {
|
||||||
|
id := r.ID()
|
||||||
|
|
||||||
|
dialAddr := fmt.Sprintf("%s:%d", f.determineDialAddr(id.LocalAddress), id.LocalPort)
|
||||||
|
|
||||||
|
outConn, err := (&net.Dialer{}).DialContext(f.ctx, "tcp", dialAddr)
|
||||||
|
if err != nil {
|
||||||
|
r.Complete(true)
|
||||||
|
f.logger.Trace("forwarder: dial error for %v: %v", id, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create wait queue for blocking syscalls
|
||||||
|
wq := waiter.Queue{}
|
||||||
|
|
||||||
|
ep, epErr := r.CreateEndpoint(&wq)
|
||||||
|
if epErr != nil {
|
||||||
|
f.logger.Error("forwarder: failed to create TCP endpoint: %v", epErr)
|
||||||
|
if err := outConn.Close(); err != nil {
|
||||||
|
f.logger.Debug("forwarder: outConn close error: %v", err)
|
||||||
|
}
|
||||||
|
r.Complete(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete the handshake
|
||||||
|
r.Complete(false)
|
||||||
|
|
||||||
|
inConn := gonet.NewTCPConn(&wq, ep)
|
||||||
|
|
||||||
|
f.logger.Trace("forwarder: established TCP connection %v", id)
|
||||||
|
|
||||||
|
go f.proxyTCP(id, inConn, outConn, ep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forwarder) proxyTCP(id stack.TransportEndpointID, inConn *gonet.TCPConn, outConn net.Conn, ep tcpip.Endpoint) {
|
||||||
|
defer func() {
|
||||||
|
if err := inConn.Close(); err != nil {
|
||||||
|
f.logger.Debug("forwarder: inConn close error: %v", err)
|
||||||
|
}
|
||||||
|
if err := outConn.Close(); err != nil {
|
||||||
|
f.logger.Debug("forwarder: outConn close error: %v", err)
|
||||||
|
}
|
||||||
|
ep.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create context for managing the proxy goroutines
|
||||||
|
ctx, cancel := context.WithCancel(f.ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
errChan := make(chan error, 2)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(outConn, inConn)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(inConn, outConn)
|
||||||
|
errChan <- err
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
f.logger.Trace("forwarder: tearing down TCP connection %v due to context done", id)
|
||||||
|
return
|
||||||
|
case err := <-errChan:
|
||||||
|
if err != nil && !isClosedError(err) {
|
||||||
|
f.logger.Error("proxyTCP: copy error: %v", err)
|
||||||
|
}
|
||||||
|
f.logger.Trace("forwarder: tearing down TCP connection %v", id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
288
client/firewall/uspfilter/forwarder/udp.go
Normal file
288
client/firewall/uspfilter/forwarder/udp.go
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
package forwarder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/stack"
|
||||||
|
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||||
|
"gvisor.dev/gvisor/pkg/waiter"
|
||||||
|
|
||||||
|
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
udpTimeout = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type udpPacketConn struct {
|
||||||
|
conn *gonet.UDPConn
|
||||||
|
outConn net.Conn
|
||||||
|
lastSeen atomic.Int64
|
||||||
|
cancel context.CancelFunc
|
||||||
|
ep tcpip.Endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
type udpForwarder struct {
|
||||||
|
sync.RWMutex
|
||||||
|
logger *nblog.Logger
|
||||||
|
conns map[stack.TransportEndpointID]*udpPacketConn
|
||||||
|
bufPool sync.Pool
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
type idleConn struct {
|
||||||
|
id stack.TransportEndpointID
|
||||||
|
conn *udpPacketConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUDPForwarder(mtu int, logger *nblog.Logger) *udpForwarder {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
f := &udpForwarder{
|
||||||
|
logger: logger,
|
||||||
|
conns: make(map[stack.TransportEndpointID]*udpPacketConn),
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
bufPool: sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
b := make([]byte, mtu)
|
||||||
|
return &b
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
go f.cleanup()
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the UDP forwarder and all active connections
|
||||||
|
func (f *udpForwarder) Stop() {
|
||||||
|
f.cancel()
|
||||||
|
|
||||||
|
f.Lock()
|
||||||
|
defer f.Unlock()
|
||||||
|
|
||||||
|
for id, conn := range f.conns {
|
||||||
|
conn.cancel()
|
||||||
|
if err := conn.conn.Close(); err != nil {
|
||||||
|
f.logger.Debug("forwarder: UDP conn close error for %v: %v", id, err)
|
||||||
|
}
|
||||||
|
if err := conn.outConn.Close(); err != nil {
|
||||||
|
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
conn.ep.Close()
|
||||||
|
delete(f.conns, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup periodically removes idle UDP connections
|
||||||
|
func (f *udpForwarder) cleanup() {
|
||||||
|
ticker := time.NewTicker(time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-f.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
var idleConns []idleConn
|
||||||
|
|
||||||
|
f.RLock()
|
||||||
|
for id, conn := range f.conns {
|
||||||
|
if conn.getIdleDuration() > udpTimeout {
|
||||||
|
idleConns = append(idleConns, idleConn{id, conn})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.RUnlock()
|
||||||
|
|
||||||
|
for _, idle := range idleConns {
|
||||||
|
idle.conn.cancel()
|
||||||
|
if err := idle.conn.conn.Close(); err != nil {
|
||||||
|
f.logger.Debug("forwarder: UDP conn close error for %v: %v", idle.id, err)
|
||||||
|
}
|
||||||
|
if err := idle.conn.outConn.Close(); err != nil {
|
||||||
|
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", idle.id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
idle.conn.ep.Close()
|
||||||
|
|
||||||
|
f.Lock()
|
||||||
|
delete(f.conns, idle.id)
|
||||||
|
f.Unlock()
|
||||||
|
|
||||||
|
f.logger.Trace("forwarder: cleaned up idle UDP connection %v", idle.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleUDP is called by the UDP forwarder for new packets
|
||||||
|
func (f *Forwarder) handleUDP(r *udp.ForwarderRequest) {
|
||||||
|
if f.ctx.Err() != nil {
|
||||||
|
f.logger.Trace("forwarder: context done, dropping UDP packet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id := r.ID()
|
||||||
|
|
||||||
|
f.udpForwarder.RLock()
|
||||||
|
_, exists := f.udpForwarder.conns[id]
|
||||||
|
f.udpForwarder.RUnlock()
|
||||||
|
if exists {
|
||||||
|
f.logger.Trace("forwarder: existing UDP connection for %v", id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dstAddr := fmt.Sprintf("%s:%d", f.determineDialAddr(id.LocalAddress), id.LocalPort)
|
||||||
|
outConn, err := (&net.Dialer{}).DialContext(f.ctx, "udp", dstAddr)
|
||||||
|
if err != nil {
|
||||||
|
f.logger.Debug("forwarder: UDP dial error for %v: %v", id, err)
|
||||||
|
// TODO: Send ICMP error message
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create wait queue for blocking syscalls
|
||||||
|
wq := waiter.Queue{}
|
||||||
|
ep, epErr := r.CreateEndpoint(&wq)
|
||||||
|
if epErr != nil {
|
||||||
|
f.logger.Debug("forwarder: failed to create UDP endpoint: %v", epErr)
|
||||||
|
if err := outConn.Close(); err != nil {
|
||||||
|
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inConn := gonet.NewUDPConn(f.stack, &wq, ep)
|
||||||
|
connCtx, connCancel := context.WithCancel(f.ctx)
|
||||||
|
|
||||||
|
pConn := &udpPacketConn{
|
||||||
|
conn: inConn,
|
||||||
|
outConn: outConn,
|
||||||
|
cancel: connCancel,
|
||||||
|
ep: ep,
|
||||||
|
}
|
||||||
|
pConn.updateLastSeen()
|
||||||
|
|
||||||
|
f.udpForwarder.Lock()
|
||||||
|
// Double-check no connection was created while we were setting up
|
||||||
|
if _, exists := f.udpForwarder.conns[id]; exists {
|
||||||
|
f.udpForwarder.Unlock()
|
||||||
|
pConn.cancel()
|
||||||
|
if err := inConn.Close(); err != nil {
|
||||||
|
f.logger.Debug("forwarder: UDP inConn close error for %v: %v", id, err)
|
||||||
|
}
|
||||||
|
if err := outConn.Close(); err != nil {
|
||||||
|
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.udpForwarder.conns[id] = pConn
|
||||||
|
f.udpForwarder.Unlock()
|
||||||
|
|
||||||
|
f.logger.Trace("forwarder: established UDP connection to %v", id)
|
||||||
|
go f.proxyUDP(connCtx, pConn, id, ep)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Forwarder) proxyUDP(ctx context.Context, pConn *udpPacketConn, id stack.TransportEndpointID, ep tcpip.Endpoint) {
|
||||||
|
defer func() {
|
||||||
|
pConn.cancel()
|
||||||
|
if err := pConn.conn.Close(); err != nil {
|
||||||
|
f.logger.Debug("forwarder: UDP inConn close error for %v: %v", id, err)
|
||||||
|
}
|
||||||
|
if err := pConn.outConn.Close(); err != nil {
|
||||||
|
f.logger.Debug("forwarder: UDP outConn close error for %v: %v", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.Close()
|
||||||
|
|
||||||
|
f.udpForwarder.Lock()
|
||||||
|
delete(f.udpForwarder.conns, id)
|
||||||
|
f.udpForwarder.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
errChan := make(chan error, 2)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
errChan <- pConn.copy(ctx, pConn.conn, pConn.outConn, &f.udpForwarder.bufPool, "outbound->inbound")
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
errChan <- pConn.copy(ctx, pConn.outConn, pConn.conn, &f.udpForwarder.bufPool, "inbound->outbound")
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
f.logger.Trace("forwarder: tearing down UDP connection %v due to context done", id)
|
||||||
|
return
|
||||||
|
case err := <-errChan:
|
||||||
|
if err != nil && !isClosedError(err) {
|
||||||
|
f.logger.Error("proxyUDP: copy error: %v", err)
|
||||||
|
}
|
||||||
|
f.logger.Trace("forwarder: tearing down UDP connection %v", id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) updateLastSeen() {
|
||||||
|
c.lastSeen.Store(time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) getIdleDuration() time.Duration {
|
||||||
|
lastSeen := time.Unix(0, c.lastSeen.Load())
|
||||||
|
return time.Since(lastSeen)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *udpPacketConn) copy(ctx context.Context, dst net.Conn, src net.Conn, bufPool *sync.Pool, direction string) error {
|
||||||
|
bufp := bufPool.Get().(*[]byte)
|
||||||
|
defer bufPool.Put(bufp)
|
||||||
|
buffer := *bufp
|
||||||
|
|
||||||
|
if err := src.SetReadDeadline(time.Now().Add(udpTimeout)); err != nil {
|
||||||
|
return fmt.Errorf("set read deadline: %w", err)
|
||||||
|
}
|
||||||
|
if err := src.SetWriteDeadline(time.Now().Add(udpTimeout)); err != nil {
|
||||||
|
return fmt.Errorf("set write deadline: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
default:
|
||||||
|
n, err := src.Read(buffer)
|
||||||
|
if err != nil {
|
||||||
|
if isTimeout(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fmt.Errorf("read from %s: %w", direction, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dst.Write(buffer[:n])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("write to %s: %w", direction, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.updateLastSeen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isClosedError(err error) bool {
|
||||||
|
return errors.Is(err, net.ErrClosed) || errors.Is(err, context.Canceled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTimeout(err error) bool {
|
||||||
|
var netErr net.Error
|
||||||
|
if errors.As(err, &netErr) {
|
||||||
|
return netErr.Timeout()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
134
client/firewall/uspfilter/localip.go
Normal file
134
client/firewall/uspfilter/localip.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package uspfilter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type localIPManager struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
// Use bitmap for IPv4 (32 bits * 2^16 = 256KB memory)
|
||||||
|
ipv4Bitmap [1 << 16]uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLocalIPManager() *localIPManager {
|
||||||
|
return &localIPManager{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *localIPManager) setBitmapBit(ip net.IP) {
|
||||||
|
ipv4 := ip.To4()
|
||||||
|
if ipv4 == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
high := (uint16(ipv4[0]) << 8) | uint16(ipv4[1])
|
||||||
|
low := (uint16(ipv4[2]) << 8) | uint16(ipv4[3])
|
||||||
|
m.ipv4Bitmap[high] |= 1 << (low % 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *localIPManager) checkBitmapBit(ip net.IP) bool {
|
||||||
|
ipv4 := ip.To4()
|
||||||
|
if ipv4 == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
high := (uint16(ipv4[0]) << 8) | uint16(ipv4[1])
|
||||||
|
low := (uint16(ipv4[2]) << 8) | uint16(ipv4[3])
|
||||||
|
return (m.ipv4Bitmap[high] & (1 << (low % 32))) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *localIPManager) processIP(ip net.IP, newIPv4Bitmap *[1 << 16]uint32, ipv4Set map[string]struct{}, ipv4Addresses *[]string) error {
|
||||||
|
if ipv4 := ip.To4(); ipv4 != nil {
|
||||||
|
high := (uint16(ipv4[0]) << 8) | uint16(ipv4[1])
|
||||||
|
low := (uint16(ipv4[2]) << 8) | uint16(ipv4[3])
|
||||||
|
if int(high) >= len(*newIPv4Bitmap) {
|
||||||
|
return fmt.Errorf("invalid IPv4 address: %s", ip)
|
||||||
|
}
|
||||||
|
ipStr := ip.String()
|
||||||
|
if _, exists := ipv4Set[ipStr]; !exists {
|
||||||
|
ipv4Set[ipStr] = struct{}{}
|
||||||
|
*ipv4Addresses = append(*ipv4Addresses, ipStr)
|
||||||
|
newIPv4Bitmap[high] |= 1 << (low % 32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *localIPManager) processInterface(iface net.Interface, newIPv4Bitmap *[1 << 16]uint32, ipv4Set map[string]struct{}, ipv4Addresses *[]string) {
|
||||||
|
addrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("get addresses for interface %s failed: %v", iface.Name, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
var ip net.IP
|
||||||
|
switch v := addr.(type) {
|
||||||
|
case *net.IPNet:
|
||||||
|
ip = v.IP
|
||||||
|
case *net.IPAddr:
|
||||||
|
ip = v.IP
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.processIP(ip, newIPv4Bitmap, ipv4Set, ipv4Addresses); err != nil {
|
||||||
|
log.Debugf("process IP failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *localIPManager) UpdateLocalIPs(iface common.IFaceMapper) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var newIPv4Bitmap [1 << 16]uint32
|
||||||
|
ipv4Set := make(map[string]struct{})
|
||||||
|
var ipv4Addresses []string
|
||||||
|
|
||||||
|
// 127.0.0.0/8
|
||||||
|
high := uint16(127) << 8
|
||||||
|
for i := uint16(0); i < 256; i++ {
|
||||||
|
newIPv4Bitmap[high|i] = 0xffffffff
|
||||||
|
}
|
||||||
|
|
||||||
|
if iface != nil {
|
||||||
|
if err := m.processIP(iface.Address().IP, &newIPv4Bitmap, ipv4Set, &ipv4Addresses); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to get interfaces: %v", err)
|
||||||
|
} else {
|
||||||
|
for _, intf := range interfaces {
|
||||||
|
m.processInterface(intf, &newIPv4Bitmap, ipv4Set, &ipv4Addresses)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.ipv4Bitmap = newIPv4Bitmap
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
log.Debugf("Local IPv4 addresses: %v", ipv4Addresses)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *localIPManager) IsLocalIP(ip net.IP) bool {
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
if ipv4 := ip.To4(); ipv4 != nil {
|
||||||
|
return m.checkBitmapBit(ipv4)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
270
client/firewall/uspfilter/localip_test.go
Normal file
270
client/firewall/uspfilter/localip_test.go
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
package uspfilter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLocalIPManager(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupAddr iface.WGAddress
|
||||||
|
testIP net.IP
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Localhost range",
|
||||||
|
setupAddr: iface.WGAddress{
|
||||||
|
IP: net.ParseIP("192.168.1.1"),
|
||||||
|
Network: &net.IPNet{
|
||||||
|
IP: net.ParseIP("192.168.1.0"),
|
||||||
|
Mask: net.CIDRMask(24, 32),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testIP: net.ParseIP("127.0.0.2"),
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Localhost standard address",
|
||||||
|
setupAddr: iface.WGAddress{
|
||||||
|
IP: net.ParseIP("192.168.1.1"),
|
||||||
|
Network: &net.IPNet{
|
||||||
|
IP: net.ParseIP("192.168.1.0"),
|
||||||
|
Mask: net.CIDRMask(24, 32),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testIP: net.ParseIP("127.0.0.1"),
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Localhost range edge",
|
||||||
|
setupAddr: iface.WGAddress{
|
||||||
|
IP: net.ParseIP("192.168.1.1"),
|
||||||
|
Network: &net.IPNet{
|
||||||
|
IP: net.ParseIP("192.168.1.0"),
|
||||||
|
Mask: net.CIDRMask(24, 32),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testIP: net.ParseIP("127.255.255.255"),
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Local IP matches",
|
||||||
|
setupAddr: iface.WGAddress{
|
||||||
|
IP: net.ParseIP("192.168.1.1"),
|
||||||
|
Network: &net.IPNet{
|
||||||
|
IP: net.ParseIP("192.168.1.0"),
|
||||||
|
Mask: net.CIDRMask(24, 32),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testIP: net.ParseIP("192.168.1.1"),
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Local IP doesn't match",
|
||||||
|
setupAddr: iface.WGAddress{
|
||||||
|
IP: net.ParseIP("192.168.1.1"),
|
||||||
|
Network: &net.IPNet{
|
||||||
|
IP: net.ParseIP("192.168.1.0"),
|
||||||
|
Mask: net.CIDRMask(24, 32),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testIP: net.ParseIP("192.168.1.2"),
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IPv6 address",
|
||||||
|
setupAddr: iface.WGAddress{
|
||||||
|
IP: net.ParseIP("fe80::1"),
|
||||||
|
Network: &net.IPNet{
|
||||||
|
IP: net.ParseIP("fe80::"),
|
||||||
|
Mask: net.CIDRMask(64, 128),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testIP: net.ParseIP("fe80::1"),
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
manager := newLocalIPManager()
|
||||||
|
|
||||||
|
mock := &IFaceMock{
|
||||||
|
AddressFunc: func() iface.WGAddress {
|
||||||
|
return tt.setupAddr
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := manager.UpdateLocalIPs(mock)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
result := manager.IsLocalIP(tt.testIP)
|
||||||
|
require.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocalIPManager_AllInterfaces(t *testing.T) {
|
||||||
|
manager := newLocalIPManager()
|
||||||
|
mock := &IFaceMock{}
|
||||||
|
|
||||||
|
// Get actual local interfaces
|
||||||
|
interfaces, err := net.Interfaces()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var tests []struct {
|
||||||
|
ip string
|
||||||
|
expected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all local interface IPs to test cases
|
||||||
|
for _, iface := range interfaces {
|
||||||
|
addrs, err := iface.Addrs()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
var ip net.IP
|
||||||
|
switch v := addr.(type) {
|
||||||
|
case *net.IPNet:
|
||||||
|
ip = v.IP
|
||||||
|
case *net.IPAddr:
|
||||||
|
ip = v.IP
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip4 := ip.To4(); ip4 != nil {
|
||||||
|
tests = append(tests, struct {
|
||||||
|
ip string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
ip: ip4.String(),
|
||||||
|
expected: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add some external IPs as negative test cases
|
||||||
|
externalIPs := []string{
|
||||||
|
"8.8.8.8",
|
||||||
|
"1.1.1.1",
|
||||||
|
"208.67.222.222",
|
||||||
|
}
|
||||||
|
for _, ip := range externalIPs {
|
||||||
|
tests = append(tests, struct {
|
||||||
|
ip string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
ip: ip,
|
||||||
|
expected: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotEmpty(t, tests, "No test cases generated")
|
||||||
|
|
||||||
|
err = manager.UpdateLocalIPs(mock)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Logf("Testing %d IPs", len(tests))
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.ip, func(t *testing.T) {
|
||||||
|
result := manager.IsLocalIP(net.ParseIP(tt.ip))
|
||||||
|
require.Equal(t, tt.expected, result, "IP: %s", tt.ip)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapImplementation is a version using map[string]struct{}
|
||||||
|
type MapImplementation struct {
|
||||||
|
localIPs map[string]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkIPChecks(b *testing.B) {
|
||||||
|
interfaces := make([]net.IP, 16)
|
||||||
|
for i := range interfaces {
|
||||||
|
interfaces[i] = net.IPv4(10, 0, byte(i>>8), byte(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup bitmap version
|
||||||
|
bitmapManager := &localIPManager{
|
||||||
|
ipv4Bitmap: [1 << 16]uint32{},
|
||||||
|
}
|
||||||
|
for _, ip := range interfaces[:8] { // Add half of IPs
|
||||||
|
bitmapManager.setBitmapBit(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup map version
|
||||||
|
mapManager := &MapImplementation{
|
||||||
|
localIPs: make(map[string]struct{}),
|
||||||
|
}
|
||||||
|
for _, ip := range interfaces[:8] {
|
||||||
|
mapManager.localIPs[ip.String()] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Run("Bitmap_Hit", func(b *testing.B) {
|
||||||
|
ip := interfaces[4]
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bitmapManager.checkBitmapBit(ip)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("Bitmap_Miss", func(b *testing.B) {
|
||||||
|
ip := interfaces[12]
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bitmapManager.checkBitmapBit(ip)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("Map_Hit", func(b *testing.B) {
|
||||||
|
ip := interfaces[4]
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
// nolint:gosimple
|
||||||
|
_, _ = mapManager.localIPs[ip.String()]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("Map_Miss", func(b *testing.B) {
|
||||||
|
ip := interfaces[12]
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
// nolint:gosimple
|
||||||
|
_, _ = mapManager.localIPs[ip.String()]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkWGPosition(b *testing.B) {
|
||||||
|
wgIP := net.ParseIP("10.10.0.1")
|
||||||
|
|
||||||
|
// Create two managers - one checks WG IP first, other checks it last
|
||||||
|
b.Run("WG_First", func(b *testing.B) {
|
||||||
|
bm := &localIPManager{ipv4Bitmap: [1 << 16]uint32{}}
|
||||||
|
bm.setBitmapBit(wgIP)
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bm.checkBitmapBit(wgIP)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("WG_Last", func(b *testing.B) {
|
||||||
|
bm := &localIPManager{ipv4Bitmap: [1 << 16]uint32{}}
|
||||||
|
// Fill with other IPs first
|
||||||
|
for i := 0; i < 15; i++ {
|
||||||
|
bm.setBitmapBit(net.IPv4(10, 0, byte(i>>8), byte(i)))
|
||||||
|
}
|
||||||
|
bm.setBitmapBit(wgIP) // Add WG IP last
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
bm.checkBitmapBit(wgIP)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
196
client/firewall/uspfilter/log/log.go
Normal file
196
client/firewall/uspfilter/log/log.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
// Package logger provides a high-performance, non-blocking logger for userspace networking
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxBatchSize = 1024 * 16 // 16KB max batch size
|
||||||
|
maxMessageSize = 1024 * 2 // 2KB per message
|
||||||
|
bufferSize = 1024 * 256 // 256KB ring buffer
|
||||||
|
defaultFlushInterval = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Level represents log severity
|
||||||
|
type Level uint32
|
||||||
|
|
||||||
|
const (
|
||||||
|
LevelPanic Level = iota
|
||||||
|
LevelFatal
|
||||||
|
LevelError
|
||||||
|
LevelWarn
|
||||||
|
LevelInfo
|
||||||
|
LevelDebug
|
||||||
|
LevelTrace
|
||||||
|
)
|
||||||
|
|
||||||
|
var levelStrings = map[Level]string{
|
||||||
|
LevelPanic: "PANC",
|
||||||
|
LevelFatal: "FATL",
|
||||||
|
LevelError: "ERRO",
|
||||||
|
LevelWarn: "WARN",
|
||||||
|
LevelInfo: "INFO",
|
||||||
|
LevelDebug: "DEBG",
|
||||||
|
LevelTrace: "TRAC",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger is a high-performance, non-blocking logger
|
||||||
|
type Logger struct {
|
||||||
|
output io.Writer
|
||||||
|
level atomic.Uint32
|
||||||
|
buffer *ringBuffer
|
||||||
|
shutdown chan struct{}
|
||||||
|
closeOnce sync.Once
|
||||||
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
// Reusable buffer pool for formatting messages
|
||||||
|
bufPool sync.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFromLogrus(logrusLogger *log.Logger) *Logger {
|
||||||
|
l := &Logger{
|
||||||
|
output: logrusLogger.Out,
|
||||||
|
buffer: newRingBuffer(bufferSize),
|
||||||
|
shutdown: make(chan struct{}),
|
||||||
|
bufPool: sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
// Pre-allocate buffer for message formatting
|
||||||
|
b := make([]byte, 0, maxMessageSize)
|
||||||
|
return &b
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
logrusLevel := logrusLogger.GetLevel()
|
||||||
|
l.level.Store(uint32(logrusLevel))
|
||||||
|
level := levelStrings[Level(logrusLevel)]
|
||||||
|
log.Debugf("New uspfilter logger created with loglevel %v", level)
|
||||||
|
|
||||||
|
l.wg.Add(1)
|
||||||
|
go l.worker()
|
||||||
|
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) SetLevel(level Level) {
|
||||||
|
l.level.Store(uint32(level))
|
||||||
|
|
||||||
|
log.Debugf("Set uspfilter logger loglevel to %v", levelStrings[level])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) formatMessage(buf *[]byte, level Level, format string, args ...interface{}) {
|
||||||
|
*buf = (*buf)[:0]
|
||||||
|
|
||||||
|
// Timestamp
|
||||||
|
*buf = time.Now().AppendFormat(*buf, "2006-01-02T15:04:05-07:00")
|
||||||
|
*buf = append(*buf, ' ')
|
||||||
|
|
||||||
|
// Level
|
||||||
|
*buf = append(*buf, levelStrings[level]...)
|
||||||
|
*buf = append(*buf, ' ')
|
||||||
|
|
||||||
|
// Message
|
||||||
|
if len(args) > 0 {
|
||||||
|
*buf = append(*buf, fmt.Sprintf(format, args...)...)
|
||||||
|
} else {
|
||||||
|
*buf = append(*buf, format...)
|
||||||
|
}
|
||||||
|
|
||||||
|
*buf = append(*buf, '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) log(level Level, format string, args ...interface{}) {
|
||||||
|
bufp := l.bufPool.Get().(*[]byte)
|
||||||
|
l.formatMessage(bufp, level, format, args...)
|
||||||
|
|
||||||
|
if len(*bufp) > maxMessageSize {
|
||||||
|
*bufp = (*bufp)[:maxMessageSize]
|
||||||
|
}
|
||||||
|
_, _ = l.buffer.Write(*bufp)
|
||||||
|
|
||||||
|
l.bufPool.Put(bufp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Error(format string, args ...interface{}) {
|
||||||
|
if l.level.Load() >= uint32(LevelError) {
|
||||||
|
l.log(LevelError, format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Warn(format string, args ...interface{}) {
|
||||||
|
if l.level.Load() >= uint32(LevelWarn) {
|
||||||
|
l.log(LevelWarn, format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Info(format string, args ...interface{}) {
|
||||||
|
if l.level.Load() >= uint32(LevelInfo) {
|
||||||
|
l.log(LevelInfo, format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Debug(format string, args ...interface{}) {
|
||||||
|
if l.level.Load() >= uint32(LevelDebug) {
|
||||||
|
l.log(LevelDebug, format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Trace(format string, args ...interface{}) {
|
||||||
|
if l.level.Load() >= uint32(LevelTrace) {
|
||||||
|
l.log(LevelTrace, format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// worker periodically flushes the buffer
|
||||||
|
func (l *Logger) worker() {
|
||||||
|
defer l.wg.Done()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(defaultFlushInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
buf := make([]byte, 0, maxBatchSize)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-l.shutdown:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
// Read accumulated messages
|
||||||
|
n, _ := l.buffer.Read(buf[:cap(buf)])
|
||||||
|
if n == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write batch
|
||||||
|
_, _ = l.output.Write(buf[:n])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop gracefully shuts down the logger
|
||||||
|
func (l *Logger) Stop(ctx context.Context) error {
|
||||||
|
done := make(chan struct{})
|
||||||
|
|
||||||
|
l.closeOnce.Do(func() {
|
||||||
|
close(l.shutdown)
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
l.wg.Wait()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-done:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
85
client/firewall/uspfilter/log/ringbuffer.go
Normal file
85
client/firewall/uspfilter/log/ringbuffer.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package log
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
// ringBuffer is a simple ring buffer implementation
|
||||||
|
type ringBuffer struct {
|
||||||
|
buf []byte
|
||||||
|
size int
|
||||||
|
r, w int64 // Read and write positions
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRingBuffer(size int) *ringBuffer {
|
||||||
|
return &ringBuffer{
|
||||||
|
buf: make([]byte, size),
|
||||||
|
size: size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ringBuffer) Write(p []byte) (n int, err error) {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
if len(p) > r.size {
|
||||||
|
p = p[:r.size]
|
||||||
|
}
|
||||||
|
|
||||||
|
n = len(p)
|
||||||
|
|
||||||
|
// Write data, handling wrap-around
|
||||||
|
pos := int(r.w % int64(r.size))
|
||||||
|
writeLen := min(len(p), r.size-pos)
|
||||||
|
copy(r.buf[pos:], p[:writeLen])
|
||||||
|
|
||||||
|
// If we have more data and need to wrap around
|
||||||
|
if writeLen < len(p) {
|
||||||
|
copy(r.buf, p[writeLen:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update write position
|
||||||
|
r.w += int64(n)
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ringBuffer) Read(p []byte) (n int, err error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
if r.w == r.r {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate available data accounting for wraparound
|
||||||
|
available := int(r.w - r.r)
|
||||||
|
if available < 0 {
|
||||||
|
available += r.size
|
||||||
|
}
|
||||||
|
available = min(available, r.size)
|
||||||
|
|
||||||
|
// Limit read to buffer size
|
||||||
|
toRead := min(available, len(p))
|
||||||
|
if toRead == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read data, handling wrap-around
|
||||||
|
pos := int(r.r % int64(r.size))
|
||||||
|
readLen := min(toRead, r.size-pos)
|
||||||
|
n = copy(p, r.buf[pos:pos+readLen])
|
||||||
|
|
||||||
|
// If we need more data and need to wrap around
|
||||||
|
if readLen < toRead {
|
||||||
|
n += copy(p[readLen:toRead], r.buf[:toRead-readLen])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update read position
|
||||||
|
r.r += int64(n)
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
@@ -2,19 +2,22 @@ package uspfilter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
"github.com/google/gopacket"
|
"github.com/google/gopacket"
|
||||||
|
|
||||||
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Rule to handle management of rules
|
// PeerRule to handle management of rules
|
||||||
type Rule struct {
|
type PeerRule struct {
|
||||||
id string
|
id string
|
||||||
ip net.IP
|
ip net.IP
|
||||||
ipLayer gopacket.LayerType
|
ipLayer gopacket.LayerType
|
||||||
matchByIP bool
|
matchByIP bool
|
||||||
protoLayer gopacket.LayerType
|
protoLayer gopacket.LayerType
|
||||||
sPort uint16
|
sPort *firewall.Port
|
||||||
dPort uint16
|
dPort *firewall.Port
|
||||||
drop bool
|
drop bool
|
||||||
comment string
|
comment string
|
||||||
|
|
||||||
@@ -22,6 +25,21 @@ type Rule struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetRuleID returns the rule id
|
// GetRuleID returns the rule id
|
||||||
func (r *Rule) GetRuleID() string {
|
func (r *PeerRule) GetRuleID() string {
|
||||||
|
return r.id
|
||||||
|
}
|
||||||
|
|
||||||
|
type RouteRule struct {
|
||||||
|
id string
|
||||||
|
sources []netip.Prefix
|
||||||
|
destination netip.Prefix
|
||||||
|
proto firewall.Protocol
|
||||||
|
srcPort *firewall.Port
|
||||||
|
dstPort *firewall.Port
|
||||||
|
action firewall.Action
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRuleID returns the rule id
|
||||||
|
func (r *RouteRule) GetRuleID() string {
|
||||||
return r.id
|
return r.id
|
||||||
}
|
}
|
||||||
|
|||||||
390
client/firewall/uspfilter/tracer.go
Normal file
390
client/firewall/uspfilter/tracer.go
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
package uspfilter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/gopacket"
|
||||||
|
"github.com/google/gopacket/layers"
|
||||||
|
|
||||||
|
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PacketStage int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StageReceived PacketStage = iota
|
||||||
|
StageConntrack
|
||||||
|
StagePeerACL
|
||||||
|
StageRouting
|
||||||
|
StageRouteACL
|
||||||
|
StageForwarding
|
||||||
|
StageCompleted
|
||||||
|
)
|
||||||
|
|
||||||
|
const msgProcessingCompleted = "Processing completed"
|
||||||
|
|
||||||
|
func (s PacketStage) String() string {
|
||||||
|
return map[PacketStage]string{
|
||||||
|
StageReceived: "Received",
|
||||||
|
StageConntrack: "Connection Tracking",
|
||||||
|
StagePeerACL: "Peer ACL",
|
||||||
|
StageRouting: "Routing",
|
||||||
|
StageRouteACL: "Route ACL",
|
||||||
|
StageForwarding: "Forwarding",
|
||||||
|
StageCompleted: "Completed",
|
||||||
|
}[s]
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwarderAction struct {
|
||||||
|
Action string
|
||||||
|
RemoteAddr string
|
||||||
|
Error error
|
||||||
|
}
|
||||||
|
|
||||||
|
type TraceResult struct {
|
||||||
|
Timestamp time.Time
|
||||||
|
Stage PacketStage
|
||||||
|
Message string
|
||||||
|
Allowed bool
|
||||||
|
ForwarderAction *ForwarderAction
|
||||||
|
}
|
||||||
|
|
||||||
|
type PacketTrace struct {
|
||||||
|
SourceIP net.IP
|
||||||
|
DestinationIP net.IP
|
||||||
|
Protocol string
|
||||||
|
SourcePort uint16
|
||||||
|
DestinationPort uint16
|
||||||
|
Direction fw.RuleDirection
|
||||||
|
Results []TraceResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type TCPState struct {
|
||||||
|
SYN bool
|
||||||
|
ACK bool
|
||||||
|
FIN bool
|
||||||
|
RST bool
|
||||||
|
PSH bool
|
||||||
|
URG bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type PacketBuilder struct {
|
||||||
|
SrcIP net.IP
|
||||||
|
DstIP net.IP
|
||||||
|
Protocol fw.Protocol
|
||||||
|
SrcPort uint16
|
||||||
|
DstPort uint16
|
||||||
|
ICMPType uint8
|
||||||
|
ICMPCode uint8
|
||||||
|
Direction fw.RuleDirection
|
||||||
|
PayloadSize int
|
||||||
|
TCPState *TCPState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PacketTrace) AddResult(stage PacketStage, message string, allowed bool) {
|
||||||
|
t.Results = append(t.Results, TraceResult{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Stage: stage,
|
||||||
|
Message: message,
|
||||||
|
Allowed: allowed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *PacketTrace) AddResultWithForwarder(stage PacketStage, message string, allowed bool, action *ForwarderAction) {
|
||||||
|
t.Results = append(t.Results, TraceResult{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
Stage: stage,
|
||||||
|
Message: message,
|
||||||
|
Allowed: allowed,
|
||||||
|
ForwarderAction: action,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PacketBuilder) Build() ([]byte, error) {
|
||||||
|
ip := p.buildIPLayer()
|
||||||
|
pktLayers := []gopacket.SerializableLayer{ip}
|
||||||
|
|
||||||
|
transportLayer, err := p.buildTransportLayer(ip)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pktLayers = append(pktLayers, transportLayer...)
|
||||||
|
|
||||||
|
if p.PayloadSize > 0 {
|
||||||
|
payload := make([]byte, p.PayloadSize)
|
||||||
|
pktLayers = append(pktLayers, gopacket.Payload(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
return serializePacket(pktLayers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PacketBuilder) buildIPLayer() *layers.IPv4 {
|
||||||
|
return &layers.IPv4{
|
||||||
|
Version: 4,
|
||||||
|
TTL: 64,
|
||||||
|
Protocol: layers.IPProtocol(getIPProtocolNumber(p.Protocol)),
|
||||||
|
SrcIP: p.SrcIP,
|
||||||
|
DstIP: p.DstIP,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PacketBuilder) buildTransportLayer(ip *layers.IPv4) ([]gopacket.SerializableLayer, error) {
|
||||||
|
switch p.Protocol {
|
||||||
|
case "tcp":
|
||||||
|
return p.buildTCPLayer(ip)
|
||||||
|
case "udp":
|
||||||
|
return p.buildUDPLayer(ip)
|
||||||
|
case "icmp":
|
||||||
|
return p.buildICMPLayer()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported protocol: %s", p.Protocol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PacketBuilder) buildTCPLayer(ip *layers.IPv4) ([]gopacket.SerializableLayer, error) {
|
||||||
|
tcp := &layers.TCP{
|
||||||
|
SrcPort: layers.TCPPort(p.SrcPort),
|
||||||
|
DstPort: layers.TCPPort(p.DstPort),
|
||||||
|
Window: 65535,
|
||||||
|
SYN: p.TCPState != nil && p.TCPState.SYN,
|
||||||
|
ACK: p.TCPState != nil && p.TCPState.ACK,
|
||||||
|
FIN: p.TCPState != nil && p.TCPState.FIN,
|
||||||
|
RST: p.TCPState != nil && p.TCPState.RST,
|
||||||
|
PSH: p.TCPState != nil && p.TCPState.PSH,
|
||||||
|
URG: p.TCPState != nil && p.TCPState.URG,
|
||||||
|
}
|
||||||
|
if err := tcp.SetNetworkLayerForChecksum(ip); err != nil {
|
||||||
|
return nil, fmt.Errorf("set network layer for TCP checksum: %w", err)
|
||||||
|
}
|
||||||
|
return []gopacket.SerializableLayer{tcp}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PacketBuilder) buildUDPLayer(ip *layers.IPv4) ([]gopacket.SerializableLayer, error) {
|
||||||
|
udp := &layers.UDP{
|
||||||
|
SrcPort: layers.UDPPort(p.SrcPort),
|
||||||
|
DstPort: layers.UDPPort(p.DstPort),
|
||||||
|
}
|
||||||
|
if err := udp.SetNetworkLayerForChecksum(ip); err != nil {
|
||||||
|
return nil, fmt.Errorf("set network layer for UDP checksum: %w", err)
|
||||||
|
}
|
||||||
|
return []gopacket.SerializableLayer{udp}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PacketBuilder) buildICMPLayer() ([]gopacket.SerializableLayer, error) {
|
||||||
|
icmp := &layers.ICMPv4{
|
||||||
|
TypeCode: layers.CreateICMPv4TypeCode(p.ICMPType, p.ICMPCode),
|
||||||
|
}
|
||||||
|
if p.ICMPType == layers.ICMPv4TypeEchoRequest || p.ICMPType == layers.ICMPv4TypeEchoReply {
|
||||||
|
icmp.Id = uint16(1)
|
||||||
|
icmp.Seq = uint16(1)
|
||||||
|
}
|
||||||
|
return []gopacket.SerializableLayer{icmp}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func serializePacket(layers []gopacket.SerializableLayer) ([]byte, error) {
|
||||||
|
buf := gopacket.NewSerializeBuffer()
|
||||||
|
opts := gopacket.SerializeOptions{
|
||||||
|
ComputeChecksums: true,
|
||||||
|
FixLengths: true,
|
||||||
|
}
|
||||||
|
if err := gopacket.SerializeLayers(buf, opts, layers...); err != nil {
|
||||||
|
return nil, fmt.Errorf("serialize packet: %w", err)
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIPProtocolNumber(protocol fw.Protocol) int {
|
||||||
|
switch protocol {
|
||||||
|
case fw.ProtocolTCP:
|
||||||
|
return int(layers.IPProtocolTCP)
|
||||||
|
case fw.ProtocolUDP:
|
||||||
|
return int(layers.IPProtocolUDP)
|
||||||
|
case fw.ProtocolICMP:
|
||||||
|
return int(layers.IPProtocolICMPv4)
|
||||||
|
default:
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) TracePacketFromBuilder(builder *PacketBuilder) (*PacketTrace, error) {
|
||||||
|
packetData, err := builder.Build()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("build packet: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.TracePacket(packetData, builder.Direction), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) TracePacket(packetData []byte, direction fw.RuleDirection) *PacketTrace {
|
||||||
|
|
||||||
|
d := m.decoders.Get().(*decoder)
|
||||||
|
defer m.decoders.Put(d)
|
||||||
|
|
||||||
|
trace := &PacketTrace{Direction: direction}
|
||||||
|
|
||||||
|
// Initial packet decoding
|
||||||
|
if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil {
|
||||||
|
trace.AddResult(StageReceived, fmt.Sprintf("Failed to decode packet: %v", err), false)
|
||||||
|
return trace
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract base packet info
|
||||||
|
srcIP, dstIP := m.extractIPs(d)
|
||||||
|
trace.SourceIP = srcIP
|
||||||
|
trace.DestinationIP = dstIP
|
||||||
|
|
||||||
|
// Determine protocol and ports
|
||||||
|
switch d.decoded[1] {
|
||||||
|
case layers.LayerTypeTCP:
|
||||||
|
trace.Protocol = "TCP"
|
||||||
|
trace.SourcePort = uint16(d.tcp.SrcPort)
|
||||||
|
trace.DestinationPort = uint16(d.tcp.DstPort)
|
||||||
|
case layers.LayerTypeUDP:
|
||||||
|
trace.Protocol = "UDP"
|
||||||
|
trace.SourcePort = uint16(d.udp.SrcPort)
|
||||||
|
trace.DestinationPort = uint16(d.udp.DstPort)
|
||||||
|
case layers.LayerTypeICMPv4:
|
||||||
|
trace.Protocol = "ICMP"
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.AddResult(StageReceived, fmt.Sprintf("Received %s packet: %s:%d -> %s:%d",
|
||||||
|
trace.Protocol, srcIP, trace.SourcePort, dstIP, trace.DestinationPort), true)
|
||||||
|
|
||||||
|
if direction == fw.RuleDirectionOUT {
|
||||||
|
return m.traceOutbound(packetData, trace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.traceInbound(packetData, trace, d, srcIP, dstIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) traceInbound(packetData []byte, trace *PacketTrace, d *decoder, srcIP net.IP, dstIP net.IP) *PacketTrace {
|
||||||
|
if m.stateful && m.handleConntrackState(trace, d, srcIP, dstIP) {
|
||||||
|
return trace
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.handleLocalDelivery(trace, packetData, d, srcIP, dstIP) {
|
||||||
|
return trace
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.handleRouting(trace) {
|
||||||
|
return trace
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.nativeRouter {
|
||||||
|
return m.handleNativeRouter(trace)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.handleRouteACLs(trace, d, srcIP, dstIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) handleConntrackState(trace *PacketTrace, d *decoder, srcIP, dstIP net.IP) bool {
|
||||||
|
allowed := m.isValidTrackedConnection(d, srcIP, dstIP)
|
||||||
|
msg := "No existing connection found"
|
||||||
|
if allowed {
|
||||||
|
msg = m.buildConntrackStateMessage(d)
|
||||||
|
trace.AddResult(StageConntrack, msg, true)
|
||||||
|
trace.AddResult(StageCompleted, "Packet allowed by connection tracking", true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
trace.AddResult(StageConntrack, msg, false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) buildConntrackStateMessage(d *decoder) string {
|
||||||
|
msg := "Matched existing connection state"
|
||||||
|
switch d.decoded[1] {
|
||||||
|
case layers.LayerTypeTCP:
|
||||||
|
flags := getTCPFlags(&d.tcp)
|
||||||
|
msg += fmt.Sprintf(" (TCP Flags: SYN=%v ACK=%v RST=%v FIN=%v)",
|
||||||
|
flags&conntrack.TCPSyn != 0,
|
||||||
|
flags&conntrack.TCPAck != 0,
|
||||||
|
flags&conntrack.TCPRst != 0,
|
||||||
|
flags&conntrack.TCPFin != 0)
|
||||||
|
case layers.LayerTypeICMPv4:
|
||||||
|
msg += fmt.Sprintf(" (ICMP ID=%d, Seq=%d)", d.icmp4.Id, d.icmp4.Seq)
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) handleLocalDelivery(trace *PacketTrace, packetData []byte, d *decoder, srcIP, dstIP net.IP) bool {
|
||||||
|
if !m.localForwarding {
|
||||||
|
trace.AddResult(StageRouting, "Local forwarding disabled", false)
|
||||||
|
trace.AddResult(StageCompleted, "Packet dropped - local forwarding disabled", false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.AddResult(StageRouting, "Packet destined for local delivery", true)
|
||||||
|
blocked := m.peerACLsBlock(srcIP, packetData, m.incomingRules, d)
|
||||||
|
|
||||||
|
msg := "Allowed by peer ACL rules"
|
||||||
|
if blocked {
|
||||||
|
msg = "Blocked by peer ACL rules"
|
||||||
|
}
|
||||||
|
trace.AddResult(StagePeerACL, msg, !blocked)
|
||||||
|
|
||||||
|
if m.netstack {
|
||||||
|
m.addForwardingResult(trace, "proxy-local", "127.0.0.1", !blocked)
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.AddResult(StageCompleted, msgProcessingCompleted, !blocked)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) handleRouting(trace *PacketTrace) bool {
|
||||||
|
if !m.routingEnabled {
|
||||||
|
trace.AddResult(StageRouting, "Routing disabled", false)
|
||||||
|
trace.AddResult(StageCompleted, "Packet dropped - routing disabled", false)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
trace.AddResult(StageRouting, "Routing enabled, checking ACLs", true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) handleNativeRouter(trace *PacketTrace) *PacketTrace {
|
||||||
|
trace.AddResult(StageRouteACL, "Using native router, skipping ACL checks", true)
|
||||||
|
trace.AddResult(StageForwarding, "Forwarding via native router", true)
|
||||||
|
trace.AddResult(StageCompleted, msgProcessingCompleted, true)
|
||||||
|
return trace
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) handleRouteACLs(trace *PacketTrace, d *decoder, srcIP, dstIP net.IP) *PacketTrace {
|
||||||
|
proto := getProtocolFromPacket(d)
|
||||||
|
srcPort, dstPort := getPortsFromPacket(d)
|
||||||
|
allowed := m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort)
|
||||||
|
|
||||||
|
msg := "Allowed by route ACLs"
|
||||||
|
if !allowed {
|
||||||
|
msg = "Blocked by route ACLs"
|
||||||
|
}
|
||||||
|
trace.AddResult(StageRouteACL, msg, allowed)
|
||||||
|
|
||||||
|
if allowed && m.forwarder != nil {
|
||||||
|
m.addForwardingResult(trace, "proxy-remote", fmt.Sprintf("%s:%d", dstIP, dstPort), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.AddResult(StageCompleted, msgProcessingCompleted, allowed)
|
||||||
|
return trace
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) addForwardingResult(trace *PacketTrace, action, remoteAddr string, allowed bool) {
|
||||||
|
fwdAction := &ForwarderAction{
|
||||||
|
Action: action,
|
||||||
|
RemoteAddr: remoteAddr,
|
||||||
|
}
|
||||||
|
trace.AddResultWithForwarder(StageForwarding,
|
||||||
|
fmt.Sprintf("Forwarding to %s", fwdAction.Action), allowed, fwdAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) traceOutbound(packetData []byte, trace *PacketTrace) *PacketTrace {
|
||||||
|
// will create or update the connection state
|
||||||
|
dropped := m.processOutgoingHooks(packetData)
|
||||||
|
if dropped {
|
||||||
|
trace.AddResult(StageCompleted, "Packet dropped by outgoing hook", false)
|
||||||
|
} else {
|
||||||
|
trace.AddResult(StageCompleted, "Packet allowed (outgoing)", true)
|
||||||
|
}
|
||||||
|
return trace
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
package uspfilter
|
package uspfilter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/google/gopacket"
|
"github.com/google/gopacket"
|
||||||
@@ -14,28 +17,48 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
firewall "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/common"
|
||||||
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/forwarder"
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
nblog "github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
||||||
|
"github.com/netbirdio/netbird/client/iface/netstack"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
const layerTypeAll = 0
|
const layerTypeAll = 0
|
||||||
|
|
||||||
const EnvDisableConntrack = "NB_DISABLE_CONNTRACK"
|
const (
|
||||||
|
// EnvDisableConntrack disables the stateful filter, replies to outbound traffic won't be allowed.
|
||||||
|
EnvDisableConntrack = "NB_DISABLE_CONNTRACK"
|
||||||
|
|
||||||
var (
|
// EnvDisableUserspaceRouting disables userspace routing, to-be-routed packets will be dropped.
|
||||||
errRouteNotSupported = fmt.Errorf("route not supported with userspace firewall")
|
EnvDisableUserspaceRouting = "NB_DISABLE_USERSPACE_ROUTING"
|
||||||
|
|
||||||
|
// EnvForceUserspaceRouter forces userspace routing even if native routing is available.
|
||||||
|
EnvForceUserspaceRouter = "NB_FORCE_USERSPACE_ROUTER"
|
||||||
|
|
||||||
|
// EnvEnableNetstackLocalForwarding enables forwarding of local traffic to the native stack when running netstack
|
||||||
|
// Leaving this on by default introduces a security risk as sockets on listening on localhost only will be accessible
|
||||||
|
EnvEnableNetstackLocalForwarding = "NB_ENABLE_NETSTACK_LOCAL_FORWARDING"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IFaceMapper defines subset methods of interface required for manager
|
|
||||||
type IFaceMapper interface {
|
|
||||||
SetFilter(device.PacketFilter) error
|
|
||||||
Address() iface.WGAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
// RuleSet is a set of rules grouped by a string key
|
// RuleSet is a set of rules grouped by a string key
|
||||||
type RuleSet map[string]Rule
|
type RuleSet map[string]PeerRule
|
||||||
|
|
||||||
|
type RouteRules []RouteRule
|
||||||
|
|
||||||
|
func (r RouteRules) Sort() {
|
||||||
|
slices.SortStableFunc(r, func(a, b RouteRule) int {
|
||||||
|
// Deny rules come first
|
||||||
|
if a.action == firewall.ActionDrop && b.action != firewall.ActionDrop {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if a.action != firewall.ActionDrop && b.action == firewall.ActionDrop {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return strings.Compare(a.id, b.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Manager userspace firewall manager
|
// Manager userspace firewall manager
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
@@ -43,17 +66,34 @@ type Manager struct {
|
|||||||
outgoingRules map[string]RuleSet
|
outgoingRules map[string]RuleSet
|
||||||
// incomingRules is used for filtering and hooks
|
// incomingRules is used for filtering and hooks
|
||||||
incomingRules map[string]RuleSet
|
incomingRules map[string]RuleSet
|
||||||
|
routeRules RouteRules
|
||||||
wgNetwork *net.IPNet
|
wgNetwork *net.IPNet
|
||||||
decoders sync.Pool
|
decoders sync.Pool
|
||||||
wgIface IFaceMapper
|
wgIface common.IFaceMapper
|
||||||
nativeFirewall firewall.Manager
|
nativeFirewall firewall.Manager
|
||||||
|
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
|
|
||||||
|
// indicates whether server routes are disabled
|
||||||
|
disableServerRoutes bool
|
||||||
|
// indicates whether we forward packets not destined for ourselves
|
||||||
|
routingEnabled bool
|
||||||
|
// indicates whether we leave forwarding and filtering to the native firewall
|
||||||
|
nativeRouter bool
|
||||||
|
// indicates whether we track outbound connections
|
||||||
stateful bool
|
stateful bool
|
||||||
|
// indicates whether wireguards runs in netstack mode
|
||||||
|
netstack bool
|
||||||
|
// indicates whether we forward local traffic to the native stack
|
||||||
|
localForwarding bool
|
||||||
|
|
||||||
|
localipmanager *localIPManager
|
||||||
|
|
||||||
udpTracker *conntrack.UDPTracker
|
udpTracker *conntrack.UDPTracker
|
||||||
icmpTracker *conntrack.ICMPTracker
|
icmpTracker *conntrack.ICMPTracker
|
||||||
tcpTracker *conntrack.TCPTracker
|
tcpTracker *conntrack.TCPTracker
|
||||||
|
forwarder *forwarder.Forwarder
|
||||||
|
logger *nblog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// decoder for packages
|
// decoder for packages
|
||||||
@@ -70,22 +110,44 @@ type decoder struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create userspace firewall manager constructor
|
// Create userspace firewall manager constructor
|
||||||
func Create(iface IFaceMapper) (*Manager, error) {
|
func Create(iface common.IFaceMapper, disableServerRoutes bool) (*Manager, error) {
|
||||||
return create(iface)
|
return create(iface, nil, disableServerRoutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateWithNativeFirewall(iface IFaceMapper, nativeFirewall firewall.Manager) (*Manager, error) {
|
func CreateWithNativeFirewall(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool) (*Manager, error) {
|
||||||
mgr, err := create(iface)
|
if nativeFirewall == nil {
|
||||||
|
return nil, errors.New("native firewall is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr, err := create(iface, nativeFirewall, disableServerRoutes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
mgr.nativeFirewall = nativeFirewall
|
|
||||||
return mgr, nil
|
return mgr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func create(iface IFaceMapper) (*Manager, error) {
|
func parseCreateEnv() (bool, bool) {
|
||||||
disableConntrack, _ := strconv.ParseBool(os.Getenv(EnvDisableConntrack))
|
var disableConntrack, enableLocalForwarding bool
|
||||||
|
var err error
|
||||||
|
if val := os.Getenv(EnvDisableConntrack); val != "" {
|
||||||
|
disableConntrack, err = strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to parse %s: %v", EnvDisableConntrack, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if val := os.Getenv(EnvEnableNetstackLocalForwarding); val != "" {
|
||||||
|
enableLocalForwarding, err = strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to parse %s: %v", EnvEnableNetstackLocalForwarding, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return disableConntrack, enableLocalForwarding
|
||||||
|
}
|
||||||
|
|
||||||
|
func create(iface common.IFaceMapper, nativeFirewall firewall.Manager, disableServerRoutes bool) (*Manager, error) {
|
||||||
|
disableConntrack, enableLocalForwarding := parseCreateEnv()
|
||||||
|
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
decoders: sync.Pool{
|
decoders: sync.Pool{
|
||||||
@@ -101,53 +163,184 @@ func create(iface IFaceMapper) (*Manager, error) {
|
|||||||
return d
|
return d
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
nativeFirewall: nativeFirewall,
|
||||||
outgoingRules: make(map[string]RuleSet),
|
outgoingRules: make(map[string]RuleSet),
|
||||||
incomingRules: make(map[string]RuleSet),
|
incomingRules: make(map[string]RuleSet),
|
||||||
wgIface: iface,
|
wgIface: iface,
|
||||||
|
localipmanager: newLocalIPManager(),
|
||||||
|
disableServerRoutes: disableServerRoutes,
|
||||||
|
routingEnabled: false,
|
||||||
stateful: !disableConntrack,
|
stateful: !disableConntrack,
|
||||||
|
logger: nblog.NewFromLogrus(log.StandardLogger()),
|
||||||
|
netstack: netstack.IsEnabled(),
|
||||||
|
// default true for non-netstack, for netstack only if explicitly enabled
|
||||||
|
localForwarding: !netstack.IsEnabled() || enableLocalForwarding,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.localipmanager.UpdateLocalIPs(iface); err != nil {
|
||||||
|
return nil, fmt.Errorf("update local IPs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only initialize trackers if stateful mode is enabled
|
|
||||||
if disableConntrack {
|
if disableConntrack {
|
||||||
log.Info("conntrack is disabled")
|
log.Info("conntrack is disabled")
|
||||||
} else {
|
} else {
|
||||||
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout)
|
m.udpTracker = conntrack.NewUDPTracker(conntrack.DefaultUDPTimeout, m.logger)
|
||||||
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout)
|
m.icmpTracker = conntrack.NewICMPTracker(conntrack.DefaultICMPTimeout, m.logger)
|
||||||
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout)
|
m.tcpTracker = conntrack.NewTCPTracker(conntrack.DefaultTCPTimeout, m.logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
// netstack needs the forwarder for local traffic
|
||||||
|
if m.netstack && m.localForwarding {
|
||||||
|
if err := m.initForwarder(); err != nil {
|
||||||
|
log.Errorf("failed to initialize forwarder: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.blockInvalidRouted(iface); err != nil {
|
||||||
|
log.Errorf("failed to block invalid routed traffic: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := iface.SetFilter(m); err != nil {
|
if err := iface.SetFilter(m); err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("set filter: %w", err)
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) blockInvalidRouted(iface common.IFaceMapper) error {
|
||||||
|
if m.forwarder == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
wgPrefix, err := netip.ParsePrefix(iface.Address().Network.String())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parse wireguard network: %w", err)
|
||||||
|
}
|
||||||
|
log.Debugf("blocking invalid routed traffic for %s", wgPrefix)
|
||||||
|
|
||||||
|
if _, err := m.AddRouteFiltering(
|
||||||
|
[]netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0)},
|
||||||
|
wgPrefix,
|
||||||
|
firewall.ProtocolALL,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
firewall.ActionDrop,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("block wg nte : %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Block networks that we're a client of
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) determineRouting() error {
|
||||||
|
var disableUspRouting, forceUserspaceRouter bool
|
||||||
|
var err error
|
||||||
|
if val := os.Getenv(EnvDisableUserspaceRouting); val != "" {
|
||||||
|
disableUspRouting, err = strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to parse %s: %v", EnvDisableUserspaceRouting, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if val := os.Getenv(EnvForceUserspaceRouter); val != "" {
|
||||||
|
forceUserspaceRouter, err = strconv.ParseBool(val)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to parse %s: %v", EnvForceUserspaceRouter, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case disableUspRouting:
|
||||||
|
m.routingEnabled = false
|
||||||
|
m.nativeRouter = false
|
||||||
|
log.Info("userspace routing is disabled")
|
||||||
|
|
||||||
|
case m.disableServerRoutes:
|
||||||
|
// if server routes are disabled we will let packets pass to the native stack
|
||||||
|
m.routingEnabled = true
|
||||||
|
m.nativeRouter = true
|
||||||
|
|
||||||
|
log.Info("server routes are disabled")
|
||||||
|
|
||||||
|
case forceUserspaceRouter:
|
||||||
|
m.routingEnabled = true
|
||||||
|
m.nativeRouter = false
|
||||||
|
|
||||||
|
log.Info("userspace routing is forced")
|
||||||
|
|
||||||
|
case !m.netstack && m.nativeFirewall != nil && m.nativeFirewall.IsServerRouteSupported():
|
||||||
|
// if the OS supports routing natively, then we don't need to filter/route ourselves
|
||||||
|
// netstack mode won't support native routing as there is no interface
|
||||||
|
|
||||||
|
m.routingEnabled = true
|
||||||
|
m.nativeRouter = true
|
||||||
|
|
||||||
|
log.Info("native routing is enabled")
|
||||||
|
|
||||||
|
default:
|
||||||
|
m.routingEnabled = true
|
||||||
|
m.nativeRouter = false
|
||||||
|
|
||||||
|
log.Info("userspace routing enabled by default")
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.routingEnabled && !m.nativeRouter {
|
||||||
|
return m.initForwarder()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initForwarder initializes the forwarder, it disables routing on errors
|
||||||
|
func (m *Manager) initForwarder() error {
|
||||||
|
if m.forwarder != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only supported in userspace mode as we need to inject packets back into wireguard directly
|
||||||
|
intf := m.wgIface.GetWGDevice()
|
||||||
|
if intf == nil {
|
||||||
|
m.routingEnabled = false
|
||||||
|
return errors.New("forwarding not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
forwarder, err := forwarder.New(m.wgIface, m.logger, m.netstack)
|
||||||
|
if err != nil {
|
||||||
|
m.routingEnabled = false
|
||||||
|
return fmt.Errorf("create forwarder: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.forwarder = forwarder
|
||||||
|
|
||||||
|
log.Debug("forwarder initialized")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) Init(*statemanager.Manager) error {
|
func (m *Manager) Init(*statemanager.Manager) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) IsServerRouteSupported() bool {
|
func (m *Manager) IsServerRouteSupported() bool {
|
||||||
if m.nativeFirewall == nil {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) AddNatRule(pair firewall.RouterPair) error {
|
func (m *Manager) AddNatRule(pair firewall.RouterPair) error {
|
||||||
if m.nativeFirewall == nil {
|
if m.nativeRouter && m.nativeFirewall != nil {
|
||||||
return errRouteNotSupported
|
|
||||||
}
|
|
||||||
return m.nativeFirewall.AddNatRule(pair)
|
return m.nativeFirewall.AddNatRule(pair)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// userspace routed packets are always SNATed to the inbound direction
|
||||||
|
// TODO: implement outbound SNAT
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// RemoveNatRule removes a routing firewall rule
|
// RemoveNatRule removes a routing firewall rule
|
||||||
func (m *Manager) RemoveNatRule(pair firewall.RouterPair) error {
|
func (m *Manager) RemoveNatRule(pair firewall.RouterPair) error {
|
||||||
if m.nativeFirewall == nil {
|
if m.nativeRouter && m.nativeFirewall != nil {
|
||||||
return errRouteNotSupported
|
|
||||||
}
|
|
||||||
return m.nativeFirewall.RemoveNatRule(pair)
|
return m.nativeFirewall.RemoveNatRule(pair)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// AddPeerFiltering rule to the firewall
|
// AddPeerFiltering rule to the firewall
|
||||||
//
|
//
|
||||||
@@ -162,7 +355,7 @@ func (m *Manager) AddPeerFiltering(
|
|||||||
_ string,
|
_ string,
|
||||||
comment string,
|
comment string,
|
||||||
) ([]firewall.Rule, error) {
|
) ([]firewall.Rule, error) {
|
||||||
r := Rule{
|
r := PeerRule{
|
||||||
id: uuid.New().String(),
|
id: uuid.New().String(),
|
||||||
ip: ip,
|
ip: ip,
|
||||||
ipLayer: layers.LayerTypeIPv6,
|
ipLayer: layers.LayerTypeIPv6,
|
||||||
@@ -179,13 +372,8 @@ func (m *Manager) AddPeerFiltering(
|
|||||||
r.matchByIP = false
|
r.matchByIP = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if sPort != nil && len(sPort.Values) == 1 {
|
r.sPort = sPort
|
||||||
r.sPort = uint16(sPort.Values[0])
|
r.dPort = dPort
|
||||||
}
|
|
||||||
|
|
||||||
if dPort != nil && len(dPort.Values) == 1 {
|
|
||||||
r.dPort = uint16(dPort.Values[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
switch proto {
|
switch proto {
|
||||||
case firewall.ProtocolTCP:
|
case firewall.ProtocolTCP:
|
||||||
@@ -210,26 +398,64 @@ func (m *Manager) AddPeerFiltering(
|
|||||||
return []firewall.Rule{&r}, nil
|
return []firewall.Rule{&r}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) AddRouteFiltering(sources []netip.Prefix, destination netip.Prefix, proto firewall.Protocol, sPort *firewall.Port, dPort *firewall.Port, action firewall.Action) (firewall.Rule, error) {
|
func (m *Manager) AddRouteFiltering(
|
||||||
if m.nativeFirewall == nil {
|
sources []netip.Prefix,
|
||||||
return nil, errRouteNotSupported
|
destination netip.Prefix,
|
||||||
}
|
proto firewall.Protocol,
|
||||||
|
sPort *firewall.Port,
|
||||||
|
dPort *firewall.Port,
|
||||||
|
action firewall.Action,
|
||||||
|
) (firewall.Rule, error) {
|
||||||
|
if m.nativeRouter && m.nativeFirewall != nil {
|
||||||
return m.nativeFirewall.AddRouteFiltering(sources, destination, proto, sPort, dPort, action)
|
return m.nativeFirewall.AddRouteFiltering(sources, destination, proto, sPort, dPort, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) DeleteRouteRule(rule firewall.Rule) error {
|
m.mutex.Lock()
|
||||||
if m.nativeFirewall == nil {
|
defer m.mutex.Unlock()
|
||||||
return errRouteNotSupported
|
|
||||||
|
ruleID := uuid.New().String()
|
||||||
|
rule := RouteRule{
|
||||||
|
id: ruleID,
|
||||||
|
sources: sources,
|
||||||
|
destination: destination,
|
||||||
|
proto: proto,
|
||||||
|
srcPort: sPort,
|
||||||
|
dstPort: dPort,
|
||||||
|
action: action,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.routeRules = append(m.routeRules, rule)
|
||||||
|
m.routeRules.Sort()
|
||||||
|
|
||||||
|
return &rule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) DeleteRouteRule(rule firewall.Rule) error {
|
||||||
|
if m.nativeRouter && m.nativeFirewall != nil {
|
||||||
return m.nativeFirewall.DeleteRouteRule(rule)
|
return m.nativeFirewall.DeleteRouteRule(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
ruleID := rule.GetRuleID()
|
||||||
|
idx := slices.IndexFunc(m.routeRules, func(r RouteRule) bool {
|
||||||
|
return r.id == ruleID
|
||||||
|
})
|
||||||
|
if idx < 0 {
|
||||||
|
return fmt.Errorf("route rule not found: %s", ruleID)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.routeRules = slices.Delete(m.routeRules, idx, idx+1)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// DeletePeerRule from the firewall by rule definition
|
// DeletePeerRule from the firewall by rule definition
|
||||||
func (m *Manager) DeletePeerRule(rule firewall.Rule) error {
|
func (m *Manager) DeletePeerRule(rule firewall.Rule) error {
|
||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
defer m.mutex.Unlock()
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
r, ok := rule.(*Rule)
|
r, ok := rule.(*PeerRule)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("delete rule: invalid rule type: %T", rule)
|
return fmt.Errorf("delete rule: invalid rule type: %T", rule)
|
||||||
}
|
}
|
||||||
@@ -260,10 +486,14 @@ func (m *Manager) DropOutgoing(packetData []byte) bool {
|
|||||||
|
|
||||||
// DropIncoming filter incoming packets
|
// DropIncoming filter incoming packets
|
||||||
func (m *Manager) DropIncoming(packetData []byte) bool {
|
func (m *Manager) DropIncoming(packetData []byte) bool {
|
||||||
return m.dropFilter(packetData, m.incomingRules)
|
return m.dropFilter(packetData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateLocalIPs updates the list of local IPs
|
||||||
|
func (m *Manager) UpdateLocalIPs() error {
|
||||||
|
return m.localipmanager.UpdateLocalIPs(m.wgIface)
|
||||||
}
|
}
|
||||||
|
|
||||||
// processOutgoingHooks processes UDP hooks for outgoing packets and tracks TCP/UDP/ICMP
|
|
||||||
func (m *Manager) processOutgoingHooks(packetData []byte) bool {
|
func (m *Manager) processOutgoingHooks(packetData []byte) bool {
|
||||||
m.mutex.RLock()
|
m.mutex.RLock()
|
||||||
defer m.mutex.RUnlock()
|
defer m.mutex.RUnlock()
|
||||||
@@ -284,18 +514,11 @@ func (m *Manager) processOutgoingHooks(packetData []byte) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always process UDP hooks
|
// Track all protocols if stateful mode is enabled
|
||||||
if d.decoded[1] == layers.LayerTypeUDP {
|
|
||||||
// Track UDP state only if enabled
|
|
||||||
if m.stateful {
|
|
||||||
m.trackUDPOutbound(d, srcIP, dstIP)
|
|
||||||
}
|
|
||||||
return m.checkUDPHooks(d, dstIP, packetData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track other protocols only if stateful mode is enabled
|
|
||||||
if m.stateful {
|
if m.stateful {
|
||||||
switch d.decoded[1] {
|
switch d.decoded[1] {
|
||||||
|
case layers.LayerTypeUDP:
|
||||||
|
m.trackUDPOutbound(d, srcIP, dstIP)
|
||||||
case layers.LayerTypeTCP:
|
case layers.LayerTypeTCP:
|
||||||
m.trackTCPOutbound(d, srcIP, dstIP)
|
m.trackTCPOutbound(d, srcIP, dstIP)
|
||||||
case layers.LayerTypeICMPv4:
|
case layers.LayerTypeICMPv4:
|
||||||
@@ -303,6 +526,11 @@ func (m *Manager) processOutgoingHooks(packetData []byte) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process UDP hooks even if stateful mode is disabled
|
||||||
|
if d.decoded[1] == layers.LayerTypeUDP {
|
||||||
|
return m.checkUDPHooks(d, dstIP, packetData)
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,7 +592,7 @@ func (m *Manager) checkUDPHooks(d *decoder, dstIP net.IP, packetData []byte) boo
|
|||||||
for _, ipKey := range []string{dstIP.String(), "0.0.0.0", "::"} {
|
for _, ipKey := range []string{dstIP.String(), "0.0.0.0", "::"} {
|
||||||
if rules, exists := m.outgoingRules[ipKey]; exists {
|
if rules, exists := m.outgoingRules[ipKey]; exists {
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
if rule.udpHook != nil && (rule.dPort == 0 || rule.dPort == uint16(d.udp.DstPort)) {
|
if rule.udpHook != nil && portsMatch(rule.dPort, uint16(d.udp.DstPort)) {
|
||||||
return rule.udpHook(packetData)
|
return rule.udpHook(packetData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,10 +612,9 @@ func (m *Manager) trackICMPOutbound(d *decoder, srcIP, dstIP net.IP) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// dropFilter implements filtering logic for incoming packets
|
// dropFilter implements filtering logic for incoming packets.
|
||||||
func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet) bool {
|
// If it returns true, the packet should be dropped.
|
||||||
// TODO: Disable router if --disable-server-router is set
|
func (m *Manager) dropFilter(packetData []byte) bool {
|
||||||
|
|
||||||
m.mutex.RLock()
|
m.mutex.RLock()
|
||||||
defer m.mutex.RUnlock()
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
@@ -400,39 +627,127 @@ func (m *Manager) dropFilter(packetData []byte, rules map[string]RuleSet) bool {
|
|||||||
|
|
||||||
srcIP, dstIP := m.extractIPs(d)
|
srcIP, dstIP := m.extractIPs(d)
|
||||||
if srcIP == nil {
|
if srcIP == nil {
|
||||||
log.Errorf("unknown layer: %v", d.decoded[0])
|
m.logger.Error("Unknown network layer: %v", d.decoded[0])
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !m.isWireguardTraffic(srcIP, dstIP) {
|
// For all inbound traffic, first check if it matches a tracked connection.
|
||||||
return false
|
// This must happen before any other filtering because the packets are statefully tracked.
|
||||||
}
|
|
||||||
|
|
||||||
// Check connection state only if enabled
|
|
||||||
if m.stateful && m.isValidTrackedConnection(d, srcIP, dstIP) {
|
if m.stateful && m.isValidTrackedConnection(d, srcIP, dstIP) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return m.applyRules(srcIP, packetData, rules, d)
|
if m.localipmanager.IsLocalIP(dstIP) {
|
||||||
|
return m.handleLocalTraffic(d, srcIP, dstIP, packetData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.handleRoutedTraffic(d, srcIP, dstIP, packetData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleLocalTraffic handles local traffic.
|
||||||
|
// If it returns true, the packet should be dropped.
|
||||||
|
func (m *Manager) handleLocalTraffic(d *decoder, srcIP, dstIP net.IP, packetData []byte) bool {
|
||||||
|
if !m.localForwarding {
|
||||||
|
m.logger.Trace("Dropping local packet (local forwarding disabled): src=%s dst=%s", srcIP, dstIP)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.peerACLsBlock(srcIP, packetData, m.incomingRules, d) {
|
||||||
|
m.logger.Trace("Dropping local packet (ACL denied): src=%s dst=%s",
|
||||||
|
srcIP, dstIP)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// if running in netstack mode we need to pass this to the forwarder
|
||||||
|
if m.netstack {
|
||||||
|
m.handleNetstackLocalTraffic(packetData)
|
||||||
|
|
||||||
|
// don't process this packet further
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
func (m *Manager) handleNetstackLocalTraffic(packetData []byte) {
|
||||||
|
if m.forwarder == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.forwarder.InjectIncomingPacket(packetData); err != nil {
|
||||||
|
m.logger.Error("Failed to inject local packet: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleRoutedTraffic handles routed traffic.
|
||||||
|
// If it returns true, the packet should be dropped.
|
||||||
|
func (m *Manager) handleRoutedTraffic(d *decoder, srcIP, dstIP net.IP, packetData []byte) bool {
|
||||||
|
// Drop if routing is disabled
|
||||||
|
if !m.routingEnabled {
|
||||||
|
m.logger.Trace("Dropping routed packet (routing disabled): src=%s dst=%s",
|
||||||
|
srcIP, dstIP)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass to native stack if native router is enabled or forced
|
||||||
|
if m.nativeRouter {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
proto := getProtocolFromPacket(d)
|
||||||
|
srcPort, dstPort := getPortsFromPacket(d)
|
||||||
|
|
||||||
|
if !m.routeACLsPass(srcIP, dstIP, proto, srcPort, dstPort) {
|
||||||
|
m.logger.Trace("Dropping routed packet (ACL denied): src=%s:%d dst=%s:%d proto=%v",
|
||||||
|
srcIP, srcPort, dstIP, dstPort, proto)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let forwarder handle the packet if it passed route ACLs
|
||||||
|
if err := m.forwarder.InjectIncomingPacket(packetData); err != nil {
|
||||||
|
m.logger.Error("Failed to inject incoming packet: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forwarded packets shouldn't reach the native stack, hence they won't be visible in a packet capture
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getProtocolFromPacket(d *decoder) firewall.Protocol {
|
||||||
|
switch d.decoded[1] {
|
||||||
|
case layers.LayerTypeTCP:
|
||||||
|
return firewall.ProtocolTCP
|
||||||
|
case layers.LayerTypeUDP:
|
||||||
|
return firewall.ProtocolUDP
|
||||||
|
case layers.LayerTypeICMPv4, layers.LayerTypeICMPv6:
|
||||||
|
return firewall.ProtocolICMP
|
||||||
|
default:
|
||||||
|
return firewall.ProtocolALL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPortsFromPacket(d *decoder) (srcPort, dstPort uint16) {
|
||||||
|
switch d.decoded[1] {
|
||||||
|
case layers.LayerTypeTCP:
|
||||||
|
return uint16(d.tcp.SrcPort), uint16(d.tcp.DstPort)
|
||||||
|
case layers.LayerTypeUDP:
|
||||||
|
return uint16(d.udp.SrcPort), uint16(d.udp.DstPort)
|
||||||
|
default:
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) isValidPacket(d *decoder, packetData []byte) bool {
|
func (m *Manager) isValidPacket(d *decoder, packetData []byte) bool {
|
||||||
if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil {
|
if err := d.parser.DecodeLayers(packetData, &d.decoded); err != nil {
|
||||||
log.Tracef("couldn't decode layer, err: %s", err)
|
m.logger.Trace("couldn't decode packet, err: %s", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(d.decoded) < 2 {
|
if len(d.decoded) < 2 {
|
||||||
log.Tracef("not enough levels in network packet")
|
m.logger.Trace("packet doesn't have network and transport layers")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) isWireguardTraffic(srcIP, dstIP net.IP) bool {
|
|
||||||
return m.wgNetwork.Contains(srcIP) && m.wgNetwork.Contains(dstIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool {
|
func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool {
|
||||||
switch d.decoded[1] {
|
switch d.decoded[1] {
|
||||||
case layers.LayerTypeTCP:
|
case layers.LayerTypeTCP:
|
||||||
@@ -467,7 +782,22 @@ func (m *Manager) isValidTrackedConnection(d *decoder, srcIP, dstIP net.IP) bool
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) applyRules(srcIP net.IP, packetData []byte, rules map[string]RuleSet, d *decoder) bool {
|
// isSpecialICMP returns true if the packet is a special ICMP packet that should be allowed
|
||||||
|
func (m *Manager) isSpecialICMP(d *decoder) bool {
|
||||||
|
if d.decoded[1] != layers.LayerTypeICMPv4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
icmpType := d.icmp4.TypeCode.Type()
|
||||||
|
return icmpType == layers.ICMPv4TypeDestinationUnreachable ||
|
||||||
|
icmpType == layers.ICMPv4TypeTimeExceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) peerACLsBlock(srcIP net.IP, packetData []byte, rules map[string]RuleSet, d *decoder) bool {
|
||||||
|
if m.isSpecialICMP(d) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if filter, ok := validateRule(srcIP, packetData, rules[srcIP.String()], d); ok {
|
if filter, ok := validateRule(srcIP, packetData, rules[srcIP.String()], d); ok {
|
||||||
return filter
|
return filter
|
||||||
}
|
}
|
||||||
@@ -484,7 +814,24 @@ func (m *Manager) applyRules(srcIP net.IP, packetData []byte, rules map[string]R
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateRule(ip net.IP, packetData []byte, rules map[string]Rule, d *decoder) (bool, bool) {
|
func portsMatch(rulePort *firewall.Port, packetPort uint16) bool {
|
||||||
|
if rulePort == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if rulePort.IsRange {
|
||||||
|
return packetPort >= rulePort.Values[0] && packetPort <= rulePort.Values[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range rulePort.Values {
|
||||||
|
if p == packetPort {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRule(ip net.IP, packetData []byte, rules map[string]PeerRule, d *decoder) (bool, bool) {
|
||||||
payloadLayer := d.decoded[1]
|
payloadLayer := d.decoded[1]
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
if rule.matchByIP && !ip.Equal(rule.ip) {
|
if rule.matchByIP && !ip.Equal(rule.ip) {
|
||||||
@@ -501,13 +848,7 @@ func validateRule(ip net.IP, packetData []byte, rules map[string]Rule, d *decode
|
|||||||
|
|
||||||
switch payloadLayer {
|
switch payloadLayer {
|
||||||
case layers.LayerTypeTCP:
|
case layers.LayerTypeTCP:
|
||||||
if rule.sPort == 0 && rule.dPort == 0 {
|
if portsMatch(rule.sPort, uint16(d.tcp.SrcPort)) && portsMatch(rule.dPort, uint16(d.tcp.DstPort)) {
|
||||||
return rule.drop, true
|
|
||||||
}
|
|
||||||
if rule.sPort != 0 && rule.sPort == uint16(d.tcp.SrcPort) {
|
|
||||||
return rule.drop, true
|
|
||||||
}
|
|
||||||
if rule.dPort != 0 && rule.dPort == uint16(d.tcp.DstPort) {
|
|
||||||
return rule.drop, true
|
return rule.drop, true
|
||||||
}
|
}
|
||||||
case layers.LayerTypeUDP:
|
case layers.LayerTypeUDP:
|
||||||
@@ -517,13 +858,7 @@ func validateRule(ip net.IP, packetData []byte, rules map[string]Rule, d *decode
|
|||||||
return rule.udpHook(packetData), true
|
return rule.udpHook(packetData), true
|
||||||
}
|
}
|
||||||
|
|
||||||
if rule.sPort == 0 && rule.dPort == 0 {
|
if portsMatch(rule.sPort, uint16(d.udp.SrcPort)) && portsMatch(rule.dPort, uint16(d.udp.DstPort)) {
|
||||||
return rule.drop, true
|
|
||||||
}
|
|
||||||
if rule.sPort != 0 && rule.sPort == uint16(d.udp.SrcPort) {
|
|
||||||
return rule.drop, true
|
|
||||||
}
|
|
||||||
if rule.dPort != 0 && rule.dPort == uint16(d.udp.DstPort) {
|
|
||||||
return rule.drop, true
|
return rule.drop, true
|
||||||
}
|
}
|
||||||
case layers.LayerTypeICMPv4, layers.LayerTypeICMPv6:
|
case layers.LayerTypeICMPv4, layers.LayerTypeICMPv6:
|
||||||
@@ -533,6 +868,51 @@ func validateRule(ip net.IP, packetData []byte, rules map[string]Rule, d *decode
|
|||||||
return false, false
|
return false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// routeACLsPass returns treu if the packet is allowed by the route ACLs
|
||||||
|
func (m *Manager) routeACLsPass(srcIP, dstIP net.IP, proto firewall.Protocol, srcPort, dstPort uint16) bool {
|
||||||
|
m.mutex.RLock()
|
||||||
|
defer m.mutex.RUnlock()
|
||||||
|
|
||||||
|
srcAddr := netip.AddrFrom4([4]byte(srcIP.To4()))
|
||||||
|
dstAddr := netip.AddrFrom4([4]byte(dstIP.To4()))
|
||||||
|
|
||||||
|
for _, rule := range m.routeRules {
|
||||||
|
if m.ruleMatches(rule, srcAddr, dstAddr, proto, srcPort, dstPort) {
|
||||||
|
return rule.action == firewall.ActionAccept
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ruleMatches(rule RouteRule, srcAddr, dstAddr netip.Addr, proto firewall.Protocol, srcPort, dstPort uint16) bool {
|
||||||
|
if !rule.destination.Contains(dstAddr) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceMatched := false
|
||||||
|
for _, src := range rule.sources {
|
||||||
|
if src.Contains(srcAddr) {
|
||||||
|
sourceMatched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !sourceMatched {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.proto != firewall.ProtocolALL && rule.proto != proto {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if proto == firewall.ProtocolTCP || proto == firewall.ProtocolUDP {
|
||||||
|
if !portsMatch(rule.srcPort, srcPort) || !portsMatch(rule.dstPort, dstPort) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// SetNetwork of the wireguard interface to which filtering applied
|
// SetNetwork of the wireguard interface to which filtering applied
|
||||||
func (m *Manager) SetNetwork(network *net.IPNet) {
|
func (m *Manager) SetNetwork(network *net.IPNet) {
|
||||||
m.wgNetwork = network
|
m.wgNetwork = network
|
||||||
@@ -544,11 +924,11 @@ func (m *Manager) SetNetwork(network *net.IPNet) {
|
|||||||
func (m *Manager) AddUDPPacketHook(
|
func (m *Manager) AddUDPPacketHook(
|
||||||
in bool, ip net.IP, dPort uint16, hook func([]byte) bool,
|
in bool, ip net.IP, dPort uint16, hook func([]byte) bool,
|
||||||
) string {
|
) string {
|
||||||
r := Rule{
|
r := PeerRule{
|
||||||
id: uuid.New().String(),
|
id: uuid.New().String(),
|
||||||
ip: ip,
|
ip: ip,
|
||||||
protoLayer: layers.LayerTypeUDP,
|
protoLayer: layers.LayerTypeUDP,
|
||||||
dPort: dPort,
|
dPort: &firewall.Port{Values: []uint16{dPort}},
|
||||||
ipLayer: layers.LayerTypeIPv6,
|
ipLayer: layers.LayerTypeIPv6,
|
||||||
comment: fmt.Sprintf("UDP Hook direction: %v, ip:%v, dport:%d", in, ip, dPort),
|
comment: fmt.Sprintf("UDP Hook direction: %v, ip:%v, dport:%d", in, ip, dPort),
|
||||||
udpHook: hook,
|
udpHook: hook,
|
||||||
@@ -561,12 +941,12 @@ func (m *Manager) AddUDPPacketHook(
|
|||||||
m.mutex.Lock()
|
m.mutex.Lock()
|
||||||
if in {
|
if in {
|
||||||
if _, ok := m.incomingRules[r.ip.String()]; !ok {
|
if _, ok := m.incomingRules[r.ip.String()]; !ok {
|
||||||
m.incomingRules[r.ip.String()] = make(map[string]Rule)
|
m.incomingRules[r.ip.String()] = make(map[string]PeerRule)
|
||||||
}
|
}
|
||||||
m.incomingRules[r.ip.String()][r.id] = r
|
m.incomingRules[r.ip.String()][r.id] = r
|
||||||
} else {
|
} else {
|
||||||
if _, ok := m.outgoingRules[r.ip.String()]; !ok {
|
if _, ok := m.outgoingRules[r.ip.String()]; !ok {
|
||||||
m.outgoingRules[r.ip.String()] = make(map[string]Rule)
|
m.outgoingRules[r.ip.String()] = make(map[string]PeerRule)
|
||||||
}
|
}
|
||||||
m.outgoingRules[r.ip.String()][r.id] = r
|
m.outgoingRules[r.ip.String()][r.id] = r
|
||||||
}
|
}
|
||||||
@@ -599,3 +979,41 @@ func (m *Manager) RemovePacketHook(hookID string) error {
|
|||||||
}
|
}
|
||||||
return fmt.Errorf("hook with given id not found")
|
return fmt.Errorf("hook with given id not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetLogLevel sets the log level for the firewall manager
|
||||||
|
func (m *Manager) SetLogLevel(level log.Level) {
|
||||||
|
if m.logger != nil {
|
||||||
|
m.logger.SetLevel(nblog.Level(level))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) EnableRouting() error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
return m.determineRouting()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) DisableRouting() error {
|
||||||
|
m.mutex.Lock()
|
||||||
|
defer m.mutex.Unlock()
|
||||||
|
|
||||||
|
if m.forwarder == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.routingEnabled = false
|
||||||
|
m.nativeRouter = false
|
||||||
|
|
||||||
|
// don't stop forwarder if in use by netstack
|
||||||
|
if m.netstack && m.localForwarding {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.forwarder.Stop()
|
||||||
|
m.forwarder = nil
|
||||||
|
|
||||||
|
log.Debug("forwarder stopped")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
//go:build uspbench
|
||||||
|
|
||||||
package uspfilter
|
package uspfilter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -112,8 +115,8 @@ func BenchmarkCoreFiltering(b *testing.B) {
|
|||||||
for i := 0; i < 1000; i++ { // Simulate realistic ruleset size
|
for i := 0; i < 1000; i++ { // Simulate realistic ruleset size
|
||||||
ip := generateRandomIPs(1)[0]
|
ip := generateRandomIPs(1)[0]
|
||||||
_, err := m.AddPeerFiltering(ip, fw.ProtocolTCP,
|
_, err := m.AddPeerFiltering(ip, fw.ProtocolTCP,
|
||||||
&fw.Port{Values: []int{1024 + i}},
|
&fw.Port{Values: []uint16{uint16(1024 + i)}},
|
||||||
&fw.Port{Values: []int{80}},
|
&fw.Port{Values: []uint16{80}},
|
||||||
fw.ActionAccept, "", "explicit return")
|
fw.ActionAccept, "", "explicit return")
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
}
|
}
|
||||||
@@ -155,7 +158,7 @@ func BenchmarkCoreFiltering(b *testing.B) {
|
|||||||
// Create manager and basic setup
|
// Create manager and basic setup
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
})
|
}, false)
|
||||||
defer b.Cleanup(func() {
|
defer b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Reset(nil))
|
require.NoError(b, manager.Reset(nil))
|
||||||
})
|
})
|
||||||
@@ -185,7 +188,7 @@ func BenchmarkCoreFiltering(b *testing.B) {
|
|||||||
// Measure inbound packet processing
|
// Measure inbound packet processing
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
manager.dropFilter(inbound, manager.incomingRules)
|
manager.dropFilter(inbound)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -200,7 +203,7 @@ func BenchmarkStateScaling(b *testing.B) {
|
|||||||
b.Run(fmt.Sprintf("conns_%d", count), func(b *testing.B) {
|
b.Run(fmt.Sprintf("conns_%d", count), func(b *testing.B) {
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
})
|
}, false)
|
||||||
b.Cleanup(func() {
|
b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Reset(nil))
|
require.NoError(b, manager.Reset(nil))
|
||||||
})
|
})
|
||||||
@@ -228,7 +231,7 @@ func BenchmarkStateScaling(b *testing.B) {
|
|||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
manager.dropFilter(testIn, manager.incomingRules)
|
manager.dropFilter(testIn)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -248,7 +251,7 @@ func BenchmarkEstablishmentOverhead(b *testing.B) {
|
|||||||
b.Run(sc.name, func(b *testing.B) {
|
b.Run(sc.name, func(b *testing.B) {
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
})
|
}, false)
|
||||||
b.Cleanup(func() {
|
b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Reset(nil))
|
require.NoError(b, manager.Reset(nil))
|
||||||
})
|
})
|
||||||
@@ -269,7 +272,7 @@ func BenchmarkEstablishmentOverhead(b *testing.B) {
|
|||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
manager.dropFilter(inbound, manager.incomingRules)
|
manager.dropFilter(inbound)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -447,7 +450,7 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
|
|||||||
b.Run(sc.name, func(b *testing.B) {
|
b.Run(sc.name, func(b *testing.B) {
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
})
|
}, false)
|
||||||
b.Cleanup(func() {
|
b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Reset(nil))
|
require.NoError(b, manager.Reset(nil))
|
||||||
})
|
})
|
||||||
@@ -472,7 +475,7 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
|
|||||||
manager.processOutgoingHooks(syn)
|
manager.processOutgoingHooks(syn)
|
||||||
// SYN-ACK
|
// SYN-ACK
|
||||||
synack := generateTCPPacketWithFlags(b, dstIP, srcIP, 80, 1024, uint16(conntrack.TCPSyn|conntrack.TCPAck))
|
synack := generateTCPPacketWithFlags(b, dstIP, srcIP, 80, 1024, uint16(conntrack.TCPSyn|conntrack.TCPAck))
|
||||||
manager.dropFilter(synack, manager.incomingRules)
|
manager.dropFilter(synack)
|
||||||
// ACK
|
// ACK
|
||||||
ack := generateTCPPacketWithFlags(b, srcIP, dstIP, 1024, 80, uint16(conntrack.TCPAck))
|
ack := generateTCPPacketWithFlags(b, srcIP, dstIP, 1024, 80, uint16(conntrack.TCPAck))
|
||||||
manager.processOutgoingHooks(ack)
|
manager.processOutgoingHooks(ack)
|
||||||
@@ -481,7 +484,7 @@ func BenchmarkRoutedNetworkReturn(b *testing.B) {
|
|||||||
|
|
||||||
b.ResetTimer()
|
b.ResetTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
manager.dropFilter(inbound, manager.incomingRules)
|
manager.dropFilter(inbound)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -574,7 +577,7 @@ func BenchmarkLongLivedConnections(b *testing.B) {
|
|||||||
|
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
})
|
}, false)
|
||||||
defer b.Cleanup(func() {
|
defer b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Reset(nil))
|
require.NoError(b, manager.Reset(nil))
|
||||||
})
|
})
|
||||||
@@ -588,7 +591,7 @@ func BenchmarkLongLivedConnections(b *testing.B) {
|
|||||||
if sc.rules {
|
if sc.rules {
|
||||||
// Single rule to allow all return traffic from port 80
|
// Single rule to allow all return traffic from port 80
|
||||||
_, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP,
|
_, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP,
|
||||||
&fw.Port{Values: []int{80}},
|
&fw.Port{Values: []uint16{80}},
|
||||||
nil,
|
nil,
|
||||||
fw.ActionAccept, "", "return traffic")
|
fw.ActionAccept, "", "return traffic")
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
@@ -618,7 +621,7 @@ func BenchmarkLongLivedConnections(b *testing.B) {
|
|||||||
// SYN-ACK
|
// SYN-ACK
|
||||||
synack := generateTCPPacketWithFlags(b, dstIPs[i], srcIPs[i],
|
synack := generateTCPPacketWithFlags(b, dstIPs[i], srcIPs[i],
|
||||||
80, uint16(1024+i), uint16(conntrack.TCPSyn|conntrack.TCPAck))
|
80, uint16(1024+i), uint16(conntrack.TCPSyn|conntrack.TCPAck))
|
||||||
manager.dropFilter(synack, manager.incomingRules)
|
manager.dropFilter(synack)
|
||||||
|
|
||||||
// ACK
|
// ACK
|
||||||
ack := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i],
|
ack := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i],
|
||||||
@@ -646,7 +649,7 @@ func BenchmarkLongLivedConnections(b *testing.B) {
|
|||||||
// First outbound data
|
// First outbound data
|
||||||
manager.processOutgoingHooks(outPackets[connIdx])
|
manager.processOutgoingHooks(outPackets[connIdx])
|
||||||
// Then inbound response - this is what we're actually measuring
|
// Then inbound response - this is what we're actually measuring
|
||||||
manager.dropFilter(inPackets[connIdx], manager.incomingRules)
|
manager.dropFilter(inPackets[connIdx])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -665,7 +668,7 @@ func BenchmarkShortLivedConnections(b *testing.B) {
|
|||||||
|
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
})
|
}, false)
|
||||||
defer b.Cleanup(func() {
|
defer b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Reset(nil))
|
require.NoError(b, manager.Reset(nil))
|
||||||
})
|
})
|
||||||
@@ -679,7 +682,7 @@ func BenchmarkShortLivedConnections(b *testing.B) {
|
|||||||
if sc.rules {
|
if sc.rules {
|
||||||
// Single rule to allow all return traffic from port 80
|
// Single rule to allow all return traffic from port 80
|
||||||
_, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP,
|
_, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP,
|
||||||
&fw.Port{Values: []int{80}},
|
&fw.Port{Values: []uint16{80}},
|
||||||
nil,
|
nil,
|
||||||
fw.ActionAccept, "", "return traffic")
|
fw.ActionAccept, "", "return traffic")
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
@@ -754,17 +757,17 @@ func BenchmarkShortLivedConnections(b *testing.B) {
|
|||||||
|
|
||||||
// Connection establishment
|
// Connection establishment
|
||||||
manager.processOutgoingHooks(p.syn)
|
manager.processOutgoingHooks(p.syn)
|
||||||
manager.dropFilter(p.synAck, manager.incomingRules)
|
manager.dropFilter(p.synAck)
|
||||||
manager.processOutgoingHooks(p.ack)
|
manager.processOutgoingHooks(p.ack)
|
||||||
|
|
||||||
// Data transfer
|
// Data transfer
|
||||||
manager.processOutgoingHooks(p.request)
|
manager.processOutgoingHooks(p.request)
|
||||||
manager.dropFilter(p.response, manager.incomingRules)
|
manager.dropFilter(p.response)
|
||||||
|
|
||||||
// Connection teardown
|
// Connection teardown
|
||||||
manager.processOutgoingHooks(p.finClient)
|
manager.processOutgoingHooks(p.finClient)
|
||||||
manager.dropFilter(p.ackServer, manager.incomingRules)
|
manager.dropFilter(p.ackServer)
|
||||||
manager.dropFilter(p.finServer, manager.incomingRules)
|
manager.dropFilter(p.finServer)
|
||||||
manager.processOutgoingHooks(p.ackClient)
|
manager.processOutgoingHooks(p.ackClient)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -784,7 +787,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) {
|
|||||||
|
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
})
|
}, false)
|
||||||
defer b.Cleanup(func() {
|
defer b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Reset(nil))
|
require.NoError(b, manager.Reset(nil))
|
||||||
})
|
})
|
||||||
@@ -797,7 +800,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) {
|
|||||||
// Setup initial state based on scenario
|
// Setup initial state based on scenario
|
||||||
if sc.rules {
|
if sc.rules {
|
||||||
_, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP,
|
_, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP,
|
||||||
&fw.Port{Values: []int{80}},
|
&fw.Port{Values: []uint16{80}},
|
||||||
nil,
|
nil,
|
||||||
fw.ActionAccept, "", "return traffic")
|
fw.ActionAccept, "", "return traffic")
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
@@ -825,7 +828,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) {
|
|||||||
|
|
||||||
synack := generateTCPPacketWithFlags(b, dstIPs[i], srcIPs[i],
|
synack := generateTCPPacketWithFlags(b, dstIPs[i], srcIPs[i],
|
||||||
80, uint16(1024+i), uint16(conntrack.TCPSyn|conntrack.TCPAck))
|
80, uint16(1024+i), uint16(conntrack.TCPSyn|conntrack.TCPAck))
|
||||||
manager.dropFilter(synack, manager.incomingRules)
|
manager.dropFilter(synack)
|
||||||
|
|
||||||
ack := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i],
|
ack := generateTCPPacketWithFlags(b, srcIPs[i], dstIPs[i],
|
||||||
uint16(1024+i), 80, uint16(conntrack.TCPAck))
|
uint16(1024+i), 80, uint16(conntrack.TCPAck))
|
||||||
@@ -852,7 +855,7 @@ func BenchmarkParallelLongLivedConnections(b *testing.B) {
|
|||||||
|
|
||||||
// Simulate bidirectional traffic
|
// Simulate bidirectional traffic
|
||||||
manager.processOutgoingHooks(outPackets[connIdx])
|
manager.processOutgoingHooks(outPackets[connIdx])
|
||||||
manager.dropFilter(inPackets[connIdx], manager.incomingRules)
|
manager.dropFilter(inPackets[connIdx])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -872,7 +875,7 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) {
|
|||||||
|
|
||||||
manager, _ := Create(&IFaceMock{
|
manager, _ := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
})
|
}, false)
|
||||||
defer b.Cleanup(func() {
|
defer b.Cleanup(func() {
|
||||||
require.NoError(b, manager.Reset(nil))
|
require.NoError(b, manager.Reset(nil))
|
||||||
})
|
})
|
||||||
@@ -884,7 +887,7 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) {
|
|||||||
|
|
||||||
if sc.rules {
|
if sc.rules {
|
||||||
_, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP,
|
_, err := manager.AddPeerFiltering(net.ParseIP("0.0.0.0"), fw.ProtocolTCP,
|
||||||
&fw.Port{Values: []int{80}},
|
&fw.Port{Values: []uint16{80}},
|
||||||
nil,
|
nil,
|
||||||
fw.ActionAccept, "", "return traffic")
|
fw.ActionAccept, "", "return traffic")
|
||||||
require.NoError(b, err)
|
require.NoError(b, err)
|
||||||
@@ -949,15 +952,15 @@ func BenchmarkParallelShortLivedConnections(b *testing.B) {
|
|||||||
|
|
||||||
// Full connection lifecycle
|
// Full connection lifecycle
|
||||||
manager.processOutgoingHooks(p.syn)
|
manager.processOutgoingHooks(p.syn)
|
||||||
manager.dropFilter(p.synAck, manager.incomingRules)
|
manager.dropFilter(p.synAck)
|
||||||
manager.processOutgoingHooks(p.ack)
|
manager.processOutgoingHooks(p.ack)
|
||||||
|
|
||||||
manager.processOutgoingHooks(p.request)
|
manager.processOutgoingHooks(p.request)
|
||||||
manager.dropFilter(p.response, manager.incomingRules)
|
manager.dropFilter(p.response)
|
||||||
|
|
||||||
manager.processOutgoingHooks(p.finClient)
|
manager.processOutgoingHooks(p.finClient)
|
||||||
manager.dropFilter(p.ackServer, manager.incomingRules)
|
manager.dropFilter(p.ackServer)
|
||||||
manager.dropFilter(p.finServer, manager.incomingRules)
|
manager.dropFilter(p.finServer)
|
||||||
manager.processOutgoingHooks(p.ackClient)
|
manager.processOutgoingHooks(p.ackClient)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -996,3 +999,72 @@ func generateTCPPacketWithFlags(b *testing.B, srcIP, dstIP net.IP, srcPort, dstP
|
|||||||
require.NoError(b, gopacket.SerializeLayers(buf, opts, ipv4, tcp, gopacket.Payload("test")))
|
require.NoError(b, gopacket.SerializeLayers(buf, opts, ipv4, tcp, gopacket.Payload("test")))
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkRouteACLs(b *testing.B) {
|
||||||
|
manager := setupRoutedManager(b, "10.10.0.100/16")
|
||||||
|
|
||||||
|
// Add several route rules to simulate real-world scenario
|
||||||
|
rules := []struct {
|
||||||
|
sources []netip.Prefix
|
||||||
|
dest netip.Prefix
|
||||||
|
proto fw.Protocol
|
||||||
|
port *fw.Port
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
sources: []netip.Prefix{netip.MustParsePrefix("100.10.0.0/16")},
|
||||||
|
dest: netip.MustParsePrefix("192.168.1.0/24"),
|
||||||
|
proto: fw.ProtocolTCP,
|
||||||
|
port: &fw.Port{Values: []uint16{80, 443}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sources: []netip.Prefix{
|
||||||
|
netip.MustParsePrefix("172.16.0.0/12"),
|
||||||
|
netip.MustParsePrefix("10.0.0.0/8"),
|
||||||
|
},
|
||||||
|
dest: netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
proto: fw.ProtocolICMP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sources: []netip.Prefix{netip.MustParsePrefix("0.0.0.0/0")},
|
||||||
|
dest: netip.MustParsePrefix("192.168.0.0/16"),
|
||||||
|
proto: fw.ProtocolUDP,
|
||||||
|
port: &fw.Port{Values: []uint16{53}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, r := range rules {
|
||||||
|
_, err := manager.AddRouteFiltering(
|
||||||
|
r.sources,
|
||||||
|
r.dest,
|
||||||
|
r.proto,
|
||||||
|
nil,
|
||||||
|
r.port,
|
||||||
|
fw.ActionAccept,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test cases that exercise different matching scenarios
|
||||||
|
cases := []struct {
|
||||||
|
srcIP string
|
||||||
|
dstIP string
|
||||||
|
proto fw.Protocol
|
||||||
|
dstPort uint16
|
||||||
|
}{
|
||||||
|
{"100.10.0.1", "192.168.1.100", fw.ProtocolTCP, 443}, // Match first rule
|
||||||
|
{"172.16.0.1", "8.8.8.8", fw.ProtocolICMP, 0}, // Match second rule
|
||||||
|
{"1.1.1.1", "192.168.1.53", fw.ProtocolUDP, 53}, // Match third rule
|
||||||
|
{"192.168.1.1", "10.0.0.1", fw.ProtocolTCP, 8080}, // No match
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
for _, tc := range cases {
|
||||||
|
srcIP := net.ParseIP(tc.srcIP)
|
||||||
|
dstIP := net.ParseIP(tc.dstIP)
|
||||||
|
manager.routeACLsPass(srcIP, dstIP, tc.proto, 0, tc.dstPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
1015
client/firewall/uspfilter/uspfilter_filter_test.go
Normal file
1015
client/firewall/uspfilter/uspfilter_filter_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,17 +9,38 @@ import (
|
|||||||
|
|
||||||
"github.com/google/gopacket"
|
"github.com/google/gopacket"
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
|
||||||
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
fw "github.com/netbirdio/netbird/client/firewall/manager"
|
||||||
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/conntrack"
|
||||||
|
"github.com/netbirdio/netbird/client/firewall/uspfilter/log"
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var logger = log.NewFromLogrus(logrus.StandardLogger())
|
||||||
|
|
||||||
type IFaceMock struct {
|
type IFaceMock struct {
|
||||||
SetFilterFunc func(device.PacketFilter) error
|
SetFilterFunc func(device.PacketFilter) error
|
||||||
AddressFunc func() iface.WGAddress
|
AddressFunc func() iface.WGAddress
|
||||||
|
GetWGDeviceFunc func() *wgdevice.Device
|
||||||
|
GetDeviceFunc func() *device.FilteredDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IFaceMock) GetWGDevice() *wgdevice.Device {
|
||||||
|
if i.GetWGDeviceFunc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return i.GetWGDeviceFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IFaceMock) GetDevice() *device.FilteredDevice {
|
||||||
|
if i.GetDeviceFunc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return i.GetDeviceFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *IFaceMock) SetFilter(iface device.PacketFilter) error {
|
func (i *IFaceMock) SetFilter(iface device.PacketFilter) error {
|
||||||
@@ -41,7 +62,7 @@ func TestManagerCreate(t *testing.T) {
|
|||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := Create(ifaceMock)
|
m, err := Create(ifaceMock, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create Manager: %v", err)
|
t.Errorf("failed to create Manager: %v", err)
|
||||||
return
|
return
|
||||||
@@ -61,7 +82,7 @@ func TestManagerAddPeerFiltering(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := Create(ifaceMock)
|
m, err := Create(ifaceMock, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create Manager: %v", err)
|
t.Errorf("failed to create Manager: %v", err)
|
||||||
return
|
return
|
||||||
@@ -69,7 +90,7 @@ func TestManagerAddPeerFiltering(t *testing.T) {
|
|||||||
|
|
||||||
ip := net.ParseIP("192.168.1.1")
|
ip := net.ParseIP("192.168.1.1")
|
||||||
proto := fw.ProtocolTCP
|
proto := fw.ProtocolTCP
|
||||||
port := &fw.Port{Values: []int{80}}
|
port := &fw.Port{Values: []uint16{80}}
|
||||||
action := fw.ActionDrop
|
action := fw.ActionDrop
|
||||||
comment := "Test rule"
|
comment := "Test rule"
|
||||||
|
|
||||||
@@ -95,7 +116,7 @@ func TestManagerDeleteRule(t *testing.T) {
|
|||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := Create(ifaceMock)
|
m, err := Create(ifaceMock, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create Manager: %v", err)
|
t.Errorf("failed to create Manager: %v", err)
|
||||||
return
|
return
|
||||||
@@ -103,7 +124,7 @@ func TestManagerDeleteRule(t *testing.T) {
|
|||||||
|
|
||||||
ip := net.ParseIP("192.168.1.1")
|
ip := net.ParseIP("192.168.1.1")
|
||||||
proto := fw.ProtocolTCP
|
proto := fw.ProtocolTCP
|
||||||
port := &fw.Port{Values: []int{80}}
|
port := &fw.Port{Values: []uint16{80}}
|
||||||
action := fw.ActionDrop
|
action := fw.ActionDrop
|
||||||
comment := "Test rule 2"
|
comment := "Test rule 2"
|
||||||
|
|
||||||
@@ -166,12 +187,12 @@ func TestAddUDPPacketHook(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
manager, err := Create(&IFaceMock{
|
manager, err := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
})
|
}, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
manager.AddUDPPacketHook(tt.in, tt.ip, tt.dPort, tt.hook)
|
manager.AddUDPPacketHook(tt.in, tt.ip, tt.dPort, tt.hook)
|
||||||
|
|
||||||
var addedRule Rule
|
var addedRule PeerRule
|
||||||
if tt.in {
|
if tt.in {
|
||||||
if len(manager.incomingRules[tt.ip.String()]) != 1 {
|
if len(manager.incomingRules[tt.ip.String()]) != 1 {
|
||||||
t.Errorf("expected 1 incoming rule, got %d", len(manager.incomingRules))
|
t.Errorf("expected 1 incoming rule, got %d", len(manager.incomingRules))
|
||||||
@@ -194,8 +215,8 @@ func TestAddUDPPacketHook(t *testing.T) {
|
|||||||
t.Errorf("expected ip %s, got %s", tt.ip, addedRule.ip)
|
t.Errorf("expected ip %s, got %s", tt.ip, addedRule.ip)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if tt.dPort != addedRule.dPort {
|
if tt.dPort != addedRule.dPort.Values[0] {
|
||||||
t.Errorf("expected dPort %d, got %d", tt.dPort, addedRule.dPort)
|
t.Errorf("expected dPort %d, got %d", tt.dPort, addedRule.dPort.Values[0])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if layers.LayerTypeUDP != addedRule.protoLayer {
|
if layers.LayerTypeUDP != addedRule.protoLayer {
|
||||||
@@ -215,7 +236,7 @@ func TestManagerReset(t *testing.T) {
|
|||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := Create(ifaceMock)
|
m, err := Create(ifaceMock, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create Manager: %v", err)
|
t.Errorf("failed to create Manager: %v", err)
|
||||||
return
|
return
|
||||||
@@ -223,7 +244,7 @@ func TestManagerReset(t *testing.T) {
|
|||||||
|
|
||||||
ip := net.ParseIP("192.168.1.1")
|
ip := net.ParseIP("192.168.1.1")
|
||||||
proto := fw.ProtocolTCP
|
proto := fw.ProtocolTCP
|
||||||
port := &fw.Port{Values: []int{80}}
|
port := &fw.Port{Values: []uint16{80}}
|
||||||
action := fw.ActionDrop
|
action := fw.ActionDrop
|
||||||
comment := "Test rule"
|
comment := "Test rule"
|
||||||
|
|
||||||
@@ -247,9 +268,18 @@ func TestManagerReset(t *testing.T) {
|
|||||||
func TestNotMatchByIP(t *testing.T) {
|
func TestNotMatchByIP(t *testing.T) {
|
||||||
ifaceMock := &IFaceMock{
|
ifaceMock := &IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
|
AddressFunc: func() iface.WGAddress {
|
||||||
|
return iface.WGAddress{
|
||||||
|
IP: net.ParseIP("100.10.0.100"),
|
||||||
|
Network: &net.IPNet{
|
||||||
|
IP: net.ParseIP("100.10.0.0"),
|
||||||
|
Mask: net.CIDRMask(16, 32),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := Create(ifaceMock)
|
m, err := Create(ifaceMock, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("failed to create Manager: %v", err)
|
t.Errorf("failed to create Manager: %v", err)
|
||||||
return
|
return
|
||||||
@@ -298,7 +328,7 @@ func TestNotMatchByIP(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.dropFilter(buf.Bytes(), m.incomingRules) {
|
if m.dropFilter(buf.Bytes()) {
|
||||||
t.Errorf("expected packet to be accepted")
|
t.Errorf("expected packet to be accepted")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -317,7 +347,7 @@ func TestRemovePacketHook(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// creating manager instance
|
// creating manager instance
|
||||||
manager, err := Create(iface)
|
manager, err := Create(iface, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create Manager: %s", err)
|
t.Fatalf("Failed to create Manager: %s", err)
|
||||||
}
|
}
|
||||||
@@ -363,7 +393,7 @@ func TestRemovePacketHook(t *testing.T) {
|
|||||||
func TestProcessOutgoingHooks(t *testing.T) {
|
func TestProcessOutgoingHooks(t *testing.T) {
|
||||||
manager, err := Create(&IFaceMock{
|
manager, err := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
})
|
}, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
manager.wgNetwork = &net.IPNet{
|
manager.wgNetwork = &net.IPNet{
|
||||||
@@ -371,7 +401,7 @@ func TestProcessOutgoingHooks(t *testing.T) {
|
|||||||
Mask: net.CIDRMask(16, 32),
|
Mask: net.CIDRMask(16, 32),
|
||||||
}
|
}
|
||||||
manager.udpTracker.Close()
|
manager.udpTracker.Close()
|
||||||
manager.udpTracker = conntrack.NewUDPTracker(100 * time.Millisecond)
|
manager.udpTracker = conntrack.NewUDPTracker(100*time.Millisecond, logger)
|
||||||
defer func() {
|
defer func() {
|
||||||
require.NoError(t, manager.Reset(nil))
|
require.NoError(t, manager.Reset(nil))
|
||||||
}()
|
}()
|
||||||
@@ -449,7 +479,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) {
|
|||||||
ifaceMock := &IFaceMock{
|
ifaceMock := &IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
}
|
}
|
||||||
manager, err := Create(ifaceMock)
|
manager, err := Create(ifaceMock, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
|
||||||
@@ -463,7 +493,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) {
|
|||||||
ip := net.ParseIP("10.20.0.100")
|
ip := net.ParseIP("10.20.0.100")
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
for i := 0; i < testMax; i++ {
|
for i := 0; i < testMax; i++ {
|
||||||
port := &fw.Port{Values: []int{1000 + i}}
|
port := &fw.Port{Values: []uint16{uint16(1000 + i)}}
|
||||||
_, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic")
|
_, err = manager.AddPeerFiltering(ip, "tcp", nil, port, fw.ActionAccept, "", "accept HTTP traffic")
|
||||||
|
|
||||||
require.NoError(t, err, "failed to add rule")
|
require.NoError(t, err, "failed to add rule")
|
||||||
@@ -476,7 +506,7 @@ func TestUSPFilterCreatePerformance(t *testing.T) {
|
|||||||
func TestStatefulFirewall_UDPTracking(t *testing.T) {
|
func TestStatefulFirewall_UDPTracking(t *testing.T) {
|
||||||
manager, err := Create(&IFaceMock{
|
manager, err := Create(&IFaceMock{
|
||||||
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
SetFilterFunc: func(device.PacketFilter) error { return nil },
|
||||||
})
|
}, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
manager.wgNetwork = &net.IPNet{
|
manager.wgNetwork = &net.IPNet{
|
||||||
@@ -485,7 +515,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
manager.udpTracker.Close() // Close the existing tracker
|
manager.udpTracker.Close() // Close the existing tracker
|
||||||
manager.udpTracker = conntrack.NewUDPTracker(200 * time.Millisecond)
|
manager.udpTracker = conntrack.NewUDPTracker(200*time.Millisecond, logger)
|
||||||
manager.decoders = sync.Pool{
|
manager.decoders = sync.Pool{
|
||||||
New: func() any {
|
New: func() any {
|
||||||
d := &decoder{
|
d := &decoder{
|
||||||
@@ -606,7 +636,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) {
|
|||||||
for _, cp := range checkPoints {
|
for _, cp := range checkPoints {
|
||||||
time.Sleep(cp.sleep)
|
time.Sleep(cp.sleep)
|
||||||
|
|
||||||
drop = manager.dropFilter(inboundBuf.Bytes(), manager.incomingRules)
|
drop = manager.dropFilter(inboundBuf.Bytes())
|
||||||
require.Equal(t, cp.shouldAllow, !drop, cp.description)
|
require.Equal(t, cp.shouldAllow, !drop, cp.description)
|
||||||
|
|
||||||
// If the connection should still be valid, verify it exists
|
// If the connection should still be valid, verify it exists
|
||||||
@@ -677,7 +707,7 @@ func TestStatefulFirewall_UDPTracking(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Verify the invalid packet is dropped
|
// Verify the invalid packet is dropped
|
||||||
drop = manager.dropFilter(testBuf.Bytes(), manager.incomingRules)
|
drop = manager.dropFilter(testBuf.Bytes())
|
||||||
require.True(t, drop, tc.description)
|
require.True(t, drop, tc.description)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
package configurer
|
package configurer
|
||||||
|
|
||||||
// WgInterfaceDefault is a default interface name of Wiretrustee
|
// WgInterfaceDefault is a default interface name of Netbird
|
||||||
const WgInterfaceDefault = "wt0"
|
const WgInterfaceDefault = "wt0"
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
package configurer
|
package configurer
|
||||||
|
|
||||||
// WgInterfaceDefault is a default interface name of Wiretrustee
|
// WgInterfaceDefault is a default interface name of Netbird
|
||||||
const WgInterfaceDefault = "utun100"
|
const WgInterfaceDefault = "utun100"
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ func toWgUserspaceString(wgCfg wgtypes.Config) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getFwmark() int {
|
func getFwmark() int {
|
||||||
if runtime.GOOS == "linux" && !nbnet.CustomRoutingDisabled() {
|
if nbnet.AdvancedRouting() {
|
||||||
return nbnet.NetbirdFwmark
|
return nbnet.NetbirdFwmark
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
package iface
|
package iface
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
)
|
)
|
||||||
@@ -15,4 +17,5 @@ type WGTunDevice interface {
|
|||||||
DeviceName() string
|
DeviceName() string
|
||||||
Close() error
|
Close() error
|
||||||
FilteredDevice() *device.FilteredDevice
|
FilteredDevice() *device.FilteredDevice
|
||||||
|
Device() *wgdevice.Device
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func (t *WGTunDevice) Create(routes []string, dns string, searchDomains []string
|
|||||||
t.filteredDevice = newDeviceFilter(tunDevice)
|
t.filteredDevice = newDeviceFilter(tunDevice)
|
||||||
|
|
||||||
log.Debugf("attaching to interface %v", name)
|
log.Debugf("attaching to interface %v", name)
|
||||||
t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[wiretrustee] "))
|
t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[netbird] "))
|
||||||
// without this property mobile devices can discover remote endpoints if the configured one was wrong.
|
// without this property mobile devices can discover remote endpoints if the configured one was wrong.
|
||||||
// this helps with support for the older NetBird clients that had a hardcoded direct mode
|
// this helps with support for the older NetBird clients that had a hardcoded direct mode
|
||||||
// t.device.DisableSomeRoamingForBrokenMobileSemantics()
|
// t.device.DisableSomeRoamingForBrokenMobileSemantics()
|
||||||
|
|||||||
@@ -117,6 +117,11 @@ func (t *TunDevice) FilteredDevice() *FilteredDevice {
|
|||||||
return t.filteredDevice
|
return t.filteredDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Device returns the wireguard device
|
||||||
|
func (t *TunDevice) Device() *device.Device {
|
||||||
|
return t.device
|
||||||
|
}
|
||||||
|
|
||||||
// assignAddr Adds IP address to the tunnel interface and network route based on the range provided
|
// assignAddr Adds IP address to the tunnel interface and network route based on the range provided
|
||||||
func (t *TunDevice) assignAddr() error {
|
func (t *TunDevice) assignAddr() error {
|
||||||
cmd := exec.Command("ifconfig", t.name, "inet", t.address.IP.String(), t.address.IP.String())
|
cmd := exec.Command("ifconfig", t.name, "inet", t.address.IP.String(), t.address.IP.String())
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func (t *TunDevice) Create() (WGConfigurer, error) {
|
|||||||
|
|
||||||
t.filteredDevice = newDeviceFilter(tunDevice)
|
t.filteredDevice = newDeviceFilter(tunDevice)
|
||||||
log.Debug("Attaching to interface")
|
log.Debug("Attaching to interface")
|
||||||
t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[wiretrustee] "))
|
t.device = device.NewDevice(t.filteredDevice, t.iceBind, device.NewLogger(wgLogLevel(), "[netbird] "))
|
||||||
// without this property mobile devices can discover remote endpoints if the configured one was wrong.
|
// without this property mobile devices can discover remote endpoints if the configured one was wrong.
|
||||||
// this helps with support for the older NetBird clients that had a hardcoded direct mode
|
// this helps with support for the older NetBird clients that had a hardcoded direct mode
|
||||||
// t.device.DisableSomeRoamingForBrokenMobileSemantics()
|
// t.device.DisableSomeRoamingForBrokenMobileSemantics()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/pion/transport/v3"
|
"github.com/pion/transport/v3"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.zx2c4.com/wireguard/device"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
@@ -33,8 +34,6 @@ type TunKernelDevice struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewKernelDevice(name string, address WGAddress, wgPort int, key string, mtu int, transportNet transport.Net) *TunKernelDevice {
|
func NewKernelDevice(name string, address WGAddress, wgPort int, key string, mtu int, transportNet transport.Net) *TunKernelDevice {
|
||||||
checkUser()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
return &TunKernelDevice{
|
return &TunKernelDevice{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
@@ -153,6 +152,11 @@ func (t *TunKernelDevice) DeviceName() string {
|
|||||||
return t.name
|
return t.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Device returns the wireguard device, not applicable for kernel devices
|
||||||
|
func (t *TunKernelDevice) Device() *device.Device {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TunKernelDevice) FilteredDevice() *FilteredDevice {
|
func (t *TunKernelDevice) FilteredDevice() *FilteredDevice {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,3 +117,8 @@ func (t *TunNetstackDevice) DeviceName() string {
|
|||||||
func (t *TunNetstackDevice) FilteredDevice() *FilteredDevice {
|
func (t *TunNetstackDevice) FilteredDevice() *FilteredDevice {
|
||||||
return t.filteredDevice
|
return t.filteredDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Device returns the wireguard device
|
||||||
|
func (t *TunNetstackDevice) Device() *device.Device {
|
||||||
|
return t.device
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ package device
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/device"
|
"golang.zx2c4.com/wireguard/device"
|
||||||
@@ -32,8 +30,6 @@ type USPDevice struct {
|
|||||||
func NewUSPDevice(name string, address WGAddress, port int, key string, mtu int, iceBind *bind.ICEBind) *USPDevice {
|
func NewUSPDevice(name string, address WGAddress, port int, key string, mtu int, iceBind *bind.ICEBind) *USPDevice {
|
||||||
log.Infof("using userspace bind mode")
|
log.Infof("using userspace bind mode")
|
||||||
|
|
||||||
checkUser()
|
|
||||||
|
|
||||||
return &USPDevice{
|
return &USPDevice{
|
||||||
name: name,
|
name: name,
|
||||||
address: address,
|
address: address,
|
||||||
@@ -128,18 +124,14 @@ func (t *USPDevice) FilteredDevice() *FilteredDevice {
|
|||||||
return t.filteredDevice
|
return t.filteredDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Device returns the wireguard device
|
||||||
|
func (t *USPDevice) Device() *device.Device {
|
||||||
|
return t.device
|
||||||
|
}
|
||||||
|
|
||||||
// assignAddr Adds IP address to the tunnel interface
|
// assignAddr Adds IP address to the tunnel interface
|
||||||
func (t *USPDevice) assignAddr() error {
|
func (t *USPDevice) assignAddr() error {
|
||||||
link := newWGLink(t.name)
|
link := newWGLink(t.name)
|
||||||
|
|
||||||
return link.assignAddr(t.address)
|
return link.assignAddr(t.address)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkUser() {
|
|
||||||
if runtime.GOOS == "freebsd" {
|
|
||||||
euid := os.Geteuid()
|
|
||||||
if euid != 0 {
|
|
||||||
log.Warn("newTunUSPDevice: on netbird must run as root to be able to assign address to the tun interface with ifconfig")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -150,6 +150,11 @@ func (t *TunDevice) FilteredDevice() *FilteredDevice {
|
|||||||
return t.filteredDevice
|
return t.filteredDevice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Device returns the wireguard device
|
||||||
|
func (t *TunDevice) Device() *device.Device {
|
||||||
|
return t.device
|
||||||
|
}
|
||||||
|
|
||||||
func (t *TunDevice) GetInterfaceGUIDString() (string, error) {
|
func (t *TunDevice) GetInterfaceGUIDString() (string, error) {
|
||||||
if t.nativeTunDevice == nil {
|
if t.nativeTunDevice == nil {
|
||||||
return "", fmt.Errorf("interface has not been initialized yet")
|
return "", fmt.Errorf("interface has not been initialized yet")
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package iface
|
package iface
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
)
|
)
|
||||||
@@ -13,4 +15,5 @@ type WGTunDevice interface {
|
|||||||
DeviceName() string
|
DeviceName() string
|
||||||
Close() error
|
Close() error
|
||||||
FilteredDevice() *device.FilteredDevice
|
FilteredDevice() *device.FilteredDevice
|
||||||
|
Device() *wgdevice.Device
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,6 +203,11 @@ func (l *Link) setAddr(ip, netmask string) error {
|
|||||||
return fmt.Errorf("set interface addr: %w", err)
|
return fmt.Errorf("set interface addr: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cmd = exec.Command("ifconfig", l.name, "inet6", "fe80::/64")
|
||||||
|
if out, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
log.Debugf("adding address command '%v' failed with output: %s", cmd.String(), out)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/errors"
|
"github.com/netbirdio/netbird/client/errors"
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
"github.com/netbirdio/netbird/client/iface/configurer"
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
@@ -203,6 +205,11 @@ func (w *WGIface) GetDevice() *device.FilteredDevice {
|
|||||||
return w.tun.FilteredDevice()
|
return w.tun.FilteredDevice()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetWGDevice returns the WireGuard device
|
||||||
|
func (w *WGIface) GetWGDevice() *wgdevice.Device {
|
||||||
|
return w.tun.Device()
|
||||||
|
}
|
||||||
|
|
||||||
// GetStats returns the last handshake time, rx and tx bytes for the given peer
|
// GetStats returns the last handshake time, rx and tx bytes for the given peer
|
||||||
func (w *WGIface) GetStats(peerKey string) (configurer.WGStats, error) {
|
func (w *WGIface) GetStats(peerKey string) (configurer.WGStats, error) {
|
||||||
return w.configurer.GetStats(peerKey)
|
return w.configurer.GetStats(peerKey)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
@@ -29,6 +30,7 @@ type MockWGIface struct {
|
|||||||
SetFilterFunc func(filter device.PacketFilter) error
|
SetFilterFunc func(filter device.PacketFilter) error
|
||||||
GetFilterFunc func() device.PacketFilter
|
GetFilterFunc func() device.PacketFilter
|
||||||
GetDeviceFunc func() *device.FilteredDevice
|
GetDeviceFunc func() *device.FilteredDevice
|
||||||
|
GetWGDeviceFunc func() *wgdevice.Device
|
||||||
GetStatsFunc func(peerKey string) (configurer.WGStats, error)
|
GetStatsFunc func(peerKey string) (configurer.WGStats, error)
|
||||||
GetInterfaceGUIDStringFunc func() (string, error)
|
GetInterfaceGUIDStringFunc func() (string, error)
|
||||||
GetProxyFunc func() wgproxy.Proxy
|
GetProxyFunc func() wgproxy.Proxy
|
||||||
@@ -102,11 +104,14 @@ func (m *MockWGIface) GetDevice() *device.FilteredDevice {
|
|||||||
return m.GetDeviceFunc()
|
return m.GetDeviceFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MockWGIface) GetWGDevice() *wgdevice.Device {
|
||||||
|
return m.GetWGDeviceFunc()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MockWGIface) GetStats(peerKey string) (configurer.WGStats, error) {
|
func (m *MockWGIface) GetStats(peerKey string) (configurer.WGStats, error) {
|
||||||
return m.GetStatsFunc(peerKey)
|
return m.GetStatsFunc(peerKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MockWGIface) GetProxy() wgproxy.Proxy {
|
func (m *MockWGIface) GetProxy() wgproxy.Proxy {
|
||||||
//TODO implement me
|
return m.GetProxyFunc()
|
||||||
panic("implement me")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
@@ -32,5 +33,6 @@ type IWGIface interface {
|
|||||||
SetFilter(filter device.PacketFilter) error
|
SetFilter(filter device.PacketFilter) error
|
||||||
GetFilter() device.PacketFilter
|
GetFilter() device.PacketFilter
|
||||||
GetDevice() *device.FilteredDevice
|
GetDevice() *device.FilteredDevice
|
||||||
|
GetWGDevice() *wgdevice.Device
|
||||||
GetStats(peerKey string) (configurer.WGStats, error)
|
GetStats(peerKey string) (configurer.WGStats, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface/bind"
|
"github.com/netbirdio/netbird/client/iface/bind"
|
||||||
@@ -30,6 +31,7 @@ type IWGIface interface {
|
|||||||
SetFilter(filter device.PacketFilter) error
|
SetFilter(filter device.PacketFilter) error
|
||||||
GetFilter() device.PacketFilter
|
GetFilter() device.PacketFilter
|
||||||
GetDevice() *device.FilteredDevice
|
GetDevice() *device.FilteredDevice
|
||||||
|
GetWGDevice() *wgdevice.Device
|
||||||
GetStats(peerKey string) (configurer.WGStats, error)
|
GetStats(peerKey string) (configurer.WGStats, error)
|
||||||
GetInterfaceGUIDString() (string, error)
|
GetInterfaceGUIDString() (string, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,13 +268,16 @@ func (d *DefaultManager) protoRuleToFirewallRule(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var port *firewall.Port
|
var port *firewall.Port
|
||||||
if r.Port != "" {
|
if !portInfoEmpty(r.PortInfo) {
|
||||||
|
port = convertPortInfo(r.PortInfo)
|
||||||
|
} else if r.Port != "" {
|
||||||
|
// old version of management, single port
|
||||||
value, err := strconv.Atoi(r.Port)
|
value, err := strconv.Atoi(r.Port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("invalid port, skipping firewall rule")
|
return "", nil, fmt.Errorf("invalid port: %w", err)
|
||||||
}
|
}
|
||||||
port = &firewall.Port{
|
port = &firewall.Port{
|
||||||
Values: []int{value},
|
Values: []uint16{uint16(value)},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,6 +305,22 @@ func (d *DefaultManager) protoRuleToFirewallRule(
|
|||||||
return ruleID, rules, nil
|
return ruleID, rules, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func portInfoEmpty(portInfo *mgmProto.PortInfo) bool {
|
||||||
|
if portInfo == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch portInfo.GetPortSelection().(type) {
|
||||||
|
case *mgmProto.PortInfo_Port:
|
||||||
|
return portInfo.GetPort() == 0
|
||||||
|
case *mgmProto.PortInfo_Range_:
|
||||||
|
r := portInfo.GetRange()
|
||||||
|
return r == nil || r.Start == 0 || r.End == 0
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DefaultManager) addInRules(
|
func (d *DefaultManager) addInRules(
|
||||||
ip net.IP,
|
ip net.IP,
|
||||||
protocol firewall.Protocol,
|
protocol firewall.Protocol,
|
||||||
@@ -488,7 +507,7 @@ func (d *DefaultManager) squashAcceptRules(
|
|||||||
|
|
||||||
// getRuleGroupingSelector takes all rule properties except IP address to build selector
|
// getRuleGroupingSelector takes all rule properties except IP address to build selector
|
||||||
func (d *DefaultManager) getRuleGroupingSelector(rule *mgmProto.FirewallRule) string {
|
func (d *DefaultManager) getRuleGroupingSelector(rule *mgmProto.FirewallRule) string {
|
||||||
return fmt.Sprintf("%v:%v:%v:%s", strconv.Itoa(int(rule.Direction)), rule.Action, rule.Protocol, rule.Port)
|
return fmt.Sprintf("%v:%v:%v:%s:%v", strconv.Itoa(int(rule.Direction)), rule.Action, rule.Protocol, rule.Port, rule.PortInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DefaultManager) rollBack(newRulePairs map[id.RuleID][]firewall.Rule) {
|
func (d *DefaultManager) rollBack(newRulePairs map[id.RuleID][]firewall.Rule) {
|
||||||
@@ -539,14 +558,14 @@ func convertPortInfo(portInfo *mgmProto.PortInfo) *firewall.Port {
|
|||||||
|
|
||||||
if portInfo.GetPort() != 0 {
|
if portInfo.GetPort() != 0 {
|
||||||
return &firewall.Port{
|
return &firewall.Port{
|
||||||
Values: []int{int(portInfo.GetPort())},
|
Values: []uint16{uint16(int(portInfo.GetPort()))},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if portInfo.GetRange() != nil {
|
if portInfo.GetRange() != nil {
|
||||||
return &firewall.Port{
|
return &firewall.Port{
|
||||||
IsRange: true,
|
IsRange: true,
|
||||||
Values: []int{int(portInfo.GetRange().Start), int(portInfo.GetRange().End)},
|
Values: []uint16{uint16(portInfo.GetRange().Start), uint16(portInfo.GetRange().End)},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,9 +49,10 @@ func TestDefaultManager(t *testing.T) {
|
|||||||
IP: ip,
|
IP: ip,
|
||||||
Network: network,
|
Network: network,
|
||||||
}).AnyTimes()
|
}).AnyTimes()
|
||||||
|
ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes()
|
||||||
|
|
||||||
// we receive one rule from the management so for testing purposes ignore it
|
// we receive one rule from the management so for testing purposes ignore it
|
||||||
fw, err := firewall.NewFirewall(ifaceMock, nil)
|
fw, err := firewall.NewFirewall(ifaceMock, nil, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("create firewall: %v", err)
|
t.Errorf("create firewall: %v", err)
|
||||||
return
|
return
|
||||||
@@ -342,9 +343,10 @@ func TestDefaultManagerEnableSSHRules(t *testing.T) {
|
|||||||
IP: ip,
|
IP: ip,
|
||||||
Network: network,
|
Network: network,
|
||||||
}).AnyTimes()
|
}).AnyTimes()
|
||||||
|
ifaceMock.EXPECT().GetWGDevice().Return(nil).AnyTimes()
|
||||||
|
|
||||||
// we receive one rule from the management so for testing purposes ignore it
|
// we receive one rule from the management so for testing purposes ignore it
|
||||||
fw, err := firewall.NewFirewall(ifaceMock, nil)
|
fw, err := firewall.NewFirewall(ifaceMock, nil, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("create firewall: %v", err)
|
t.Errorf("create firewall: %v", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
|
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
wgdevice "golang.zx2c4.com/wireguard/device"
|
||||||
|
|
||||||
iface "github.com/netbirdio/netbird/client/iface"
|
iface "github.com/netbirdio/netbird/client/iface"
|
||||||
"github.com/netbirdio/netbird/client/iface/device"
|
"github.com/netbirdio/netbird/client/iface/device"
|
||||||
)
|
)
|
||||||
@@ -90,3 +92,31 @@ func (mr *MockIFaceMapperMockRecorder) SetFilter(arg0 interface{}) *gomock.Call
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFilter", reflect.TypeOf((*MockIFaceMapper)(nil).SetFilter), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetFilter", reflect.TypeOf((*MockIFaceMapper)(nil).SetFilter), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetDevice mocks base method.
|
||||||
|
func (m *MockIFaceMapper) GetDevice() *device.FilteredDevice {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetDevice")
|
||||||
|
ret0, _ := ret[0].(*device.FilteredDevice)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDevice indicates an expected call of GetDevice.
|
||||||
|
func (mr *MockIFaceMapperMockRecorder) GetDevice() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDevice", reflect.TypeOf((*MockIFaceMapper)(nil).GetDevice))
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWGDevice mocks base method.
|
||||||
|
func (m *MockIFaceMapper) GetWGDevice() *wgdevice.Device {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "GetWGDevice")
|
||||||
|
ret0, _ := ret[0].(*wgdevice.Device)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWGDevice indicates an expected call of GetWGDevice.
|
||||||
|
func (mr *MockIFaceMapperMockRecorder) GetWGDevice() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWGDevice", reflect.TypeOf((*MockIFaceMapper)(nil).GetWGDevice))
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package auth
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -11,7 +13,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal"
|
"github.com/netbirdio/netbird/client/internal"
|
||||||
|
"github.com/netbirdio/netbird/util/embeddedroots"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HostedGrantType grant type for device flow on Hosted
|
// HostedGrantType grant type for device flow on Hosted
|
||||||
@@ -56,6 +61,18 @@ func NewDeviceAuthorizationFlow(config internal.DeviceAuthProviderConfig) (*Devi
|
|||||||
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
httpTransport := http.DefaultTransport.(*http.Transport).Clone()
|
||||||
httpTransport.MaxIdleConns = 5
|
httpTransport.MaxIdleConns = 5
|
||||||
|
|
||||||
|
certPool, err := x509.SystemCertPool()
|
||||||
|
if err != nil || certPool == nil {
|
||||||
|
log.Debugf("System cert pool not available; falling back to embedded cert, error: %v", err)
|
||||||
|
certPool = embeddedroots.Get()
|
||||||
|
} else {
|
||||||
|
log.Debug("Using system certificate pool.")
|
||||||
|
}
|
||||||
|
|
||||||
|
httpTransport.TLSClientConfig = &tls.Config{
|
||||||
|
RootCAs: certPool,
|
||||||
|
}
|
||||||
|
|
||||||
httpClient := &http.Client{
|
httpClient := &http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 10 * time.Second,
|
||||||
Transport: httpTransport,
|
Transport: httpTransport,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
relayClient "github.com/netbirdio/netbird/relay/client"
|
relayClient "github.com/netbirdio/netbird/relay/client"
|
||||||
signal "github.com/netbirdio/netbird/signal/client"
|
signal "github.com/netbirdio/netbird/signal/client"
|
||||||
"github.com/netbirdio/netbird/util"
|
"github.com/netbirdio/netbird/util"
|
||||||
|
nbnet "github.com/netbirdio/netbird/util/net"
|
||||||
"github.com/netbirdio/netbird/version"
|
"github.com/netbirdio/netbird/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -59,13 +60,8 @@ func NewConnectClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run with main logic.
|
// Run with main logic.
|
||||||
func (c *ConnectClient) Run() error {
|
func (c *ConnectClient) Run(runningChan chan error) error {
|
||||||
return c.run(MobileDependency{}, nil, nil)
|
return c.run(MobileDependency{}, runningChan)
|
||||||
}
|
|
||||||
|
|
||||||
// RunWithProbes runs the client's main logic with probes attached
|
|
||||||
func (c *ConnectClient) RunWithProbes(probes *ProbeHolder, runningChan chan error) error {
|
|
||||||
return c.run(MobileDependency{}, probes, runningChan)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunOnAndroid with main logic on mobile system
|
// RunOnAndroid with main logic on mobile system
|
||||||
@@ -84,7 +80,7 @@ func (c *ConnectClient) RunOnAndroid(
|
|||||||
HostDNSAddresses: dnsAddresses,
|
HostDNSAddresses: dnsAddresses,
|
||||||
DnsReadyListener: dnsReadyListener,
|
DnsReadyListener: dnsReadyListener,
|
||||||
}
|
}
|
||||||
return c.run(mobileDependency, nil, nil)
|
return c.run(mobileDependency, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConnectClient) RunOniOS(
|
func (c *ConnectClient) RunOniOS(
|
||||||
@@ -102,10 +98,10 @@ func (c *ConnectClient) RunOniOS(
|
|||||||
DnsManager: dnsManager,
|
DnsManager: dnsManager,
|
||||||
StateFilePath: stateFilePath,
|
StateFilePath: stateFilePath,
|
||||||
}
|
}
|
||||||
return c.run(mobileDependency, nil, nil)
|
return c.run(mobileDependency, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHolder, runningChan chan error) error {
|
func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan error) error {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
log.Panicf("Panic occurred: %v, stack trace: %s", r, string(debug.Stack()))
|
log.Panicf("Panic occurred: %v, stack trace: %s", r, string(debug.Stack()))
|
||||||
@@ -114,6 +110,8 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold
|
|||||||
|
|
||||||
log.Infof("starting NetBird client version %s on %s/%s", version.NetbirdVersion(), runtime.GOOS, runtime.GOARCH)
|
log.Infof("starting NetBird client version %s on %s/%s", version.NetbirdVersion(), runtime.GOOS, runtime.GOARCH)
|
||||||
|
|
||||||
|
nbnet.Init()
|
||||||
|
|
||||||
backOff := &backoff.ExponentialBackOff{
|
backOff := &backoff.ExponentialBackOff{
|
||||||
InitialInterval: time.Second,
|
InitialInterval: time.Second,
|
||||||
RandomizationFactor: 1,
|
RandomizationFactor: 1,
|
||||||
@@ -182,7 +180,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// connect (just a connection, no stream yet) and login to Management Service to get an initial global Wiretrustee config
|
// connect (just a connection, no stream yet) and login to Management Service to get an initial global Netbird config
|
||||||
loginResp, err := loginToManagement(engineCtx, mgmClient, publicSSHKey, c.config)
|
loginResp, err := loginToManagement(engineCtx, mgmClient, publicSSHKey, c.config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug(err)
|
log.Debug(err)
|
||||||
@@ -204,8 +202,8 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold
|
|||||||
c.statusRecorder.UpdateLocalPeerState(localPeerState)
|
c.statusRecorder.UpdateLocalPeerState(localPeerState)
|
||||||
|
|
||||||
signalURL := fmt.Sprintf("%s://%s",
|
signalURL := fmt.Sprintf("%s://%s",
|
||||||
strings.ToLower(loginResp.GetWiretrusteeConfig().GetSignal().GetProtocol().String()),
|
strings.ToLower(loginResp.GetNetbirdConfig().GetSignal().GetProtocol().String()),
|
||||||
loginResp.GetWiretrusteeConfig().GetSignal().GetUri(),
|
loginResp.GetNetbirdConfig().GetSignal().GetUri(),
|
||||||
)
|
)
|
||||||
|
|
||||||
c.statusRecorder.UpdateSignalAddress(signalURL)
|
c.statusRecorder.UpdateSignalAddress(signalURL)
|
||||||
@@ -216,8 +214,8 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold
|
|||||||
c.statusRecorder.MarkSignalDisconnected(err)
|
c.statusRecorder.MarkSignalDisconnected(err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// with the global Wiretrustee config in hand connect (just a connection, no stream yet) Signal
|
// with the global Netbird config in hand connect (just a connection, no stream yet) Signal
|
||||||
signalClient, err := connectToSignal(engineCtx, loginResp.GetWiretrusteeConfig(), myPrivateKey)
|
signalClient, err := connectToSignal(engineCtx, loginResp.GetNetbirdConfig(), myPrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
return wrapErr(err)
|
return wrapErr(err)
|
||||||
@@ -261,7 +259,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold
|
|||||||
checks := loginResp.GetChecks()
|
checks := loginResp.GetChecks()
|
||||||
|
|
||||||
c.engineMutex.Lock()
|
c.engineMutex.Lock()
|
||||||
c.engine = NewEngineWithProbes(engineCtx, cancel, signalClient, mgmClient, relayManager, engineConfig, mobileDependency, c.statusRecorder, probes, checks)
|
c.engine = NewEngine(engineCtx, cancel, signalClient, mgmClient, relayManager, engineConfig, mobileDependency, c.statusRecorder, checks)
|
||||||
c.engine.SetNetworkMapPersistence(c.persistNetworkMap)
|
c.engine.SetNetworkMapPersistence(c.persistNetworkMap)
|
||||||
c.engineMutex.Unlock()
|
c.engineMutex.Unlock()
|
||||||
|
|
||||||
@@ -316,7 +314,7 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, probes *ProbeHold
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseRelayInfo(loginResp *mgmProto.LoginResponse) ([]string, *hmac.Token) {
|
func parseRelayInfo(loginResp *mgmProto.LoginResponse) ([]string, *hmac.Token) {
|
||||||
relayCfg := loginResp.GetWiretrusteeConfig().GetRelay()
|
relayCfg := loginResp.GetNetbirdConfig().GetRelay()
|
||||||
if relayCfg == nil {
|
if relayCfg == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -445,7 +443,7 @@ func createEngineConfig(key wgtypes.Key, config *Config, peerConfig *mgmProto.Pe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// connectToSignal creates Signal Service client and established a connection
|
// connectToSignal creates Signal Service client and established a connection
|
||||||
func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig, ourPrivateKey wgtypes.Key) (*signal.GrpcClient, error) {
|
func connectToSignal(ctx context.Context, wtConfig *mgmProto.NetbirdConfig, ourPrivateKey wgtypes.Key) (*signal.GrpcClient, error) {
|
||||||
var sigTLSEnabled bool
|
var sigTLSEnabled bool
|
||||||
if wtConfig.Signal.Protocol == mgmProto.HostConfig_HTTPS {
|
if wtConfig.Signal.Protocol == mgmProto.HostConfig_HTTPS {
|
||||||
sigTLSEnabled = true
|
sigTLSEnabled = true
|
||||||
@@ -462,7 +460,7 @@ func connectToSignal(ctx context.Context, wtConfig *mgmProto.WiretrusteeConfig,
|
|||||||
return signalClient, nil
|
return signalClient, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loginToManagement creates Management Services client, establishes a connection, logs-in and gets a global Wiretrustee config (signal, turn, stun hosts, etc)
|
// loginToManagement creates Management Services client, establishes a connection, logs-in and gets a global Netbird config (signal, turn, stun hosts, etc)
|
||||||
func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) {
|
func loginToManagement(ctx context.Context, client mgm.Client, pubSSHKey []byte, config *Config) (*mgmProto.LoginResponse, error) {
|
||||||
|
|
||||||
serverPublicKey, err := client.GetServerPublicKey()
|
serverPublicKey, err := client.GetServerPublicKey()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
const (
|
const (
|
||||||
PriorityDNSRoute = 100
|
PriorityDNSRoute = 100
|
||||||
PriorityMatchDomain = 50
|
PriorityMatchDomain = 50
|
||||||
PriorityDefault = 0
|
PriorityDefault = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
type SubdomainMatcher interface {
|
type SubdomainMatcher interface {
|
||||||
@@ -26,7 +26,6 @@ type HandlerEntry struct {
|
|||||||
Pattern string
|
Pattern string
|
||||||
OrigPattern string
|
OrigPattern string
|
||||||
IsWildcard bool
|
IsWildcard bool
|
||||||
StopHandler handlerWithStop
|
|
||||||
MatchSubdomains bool
|
MatchSubdomains bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +63,7 @@ func (w *ResponseWriterChain) GetOrigPattern() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddHandler adds a new handler to the chain, replacing any existing handler with the same pattern and priority
|
// AddHandler adds a new handler to the chain, replacing any existing handler with the same pattern and priority
|
||||||
func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority int, stopHandler handlerWithStop) {
|
func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority int) {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
@@ -78,9 +77,6 @@ func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority
|
|||||||
// First remove any existing handler with same pattern (case-insensitive) and priority
|
// First remove any existing handler with same pattern (case-insensitive) and priority
|
||||||
for i := len(c.handlers) - 1; i >= 0; i-- {
|
for i := len(c.handlers) - 1; i >= 0; i-- {
|
||||||
if strings.EqualFold(c.handlers[i].OrigPattern, origPattern) && c.handlers[i].Priority == priority {
|
if strings.EqualFold(c.handlers[i].OrigPattern, origPattern) && c.handlers[i].Priority == priority {
|
||||||
if c.handlers[i].StopHandler != nil {
|
|
||||||
c.handlers[i].StopHandler.stop()
|
|
||||||
}
|
|
||||||
c.handlers = append(c.handlers[:i], c.handlers[i+1:]...)
|
c.handlers = append(c.handlers[:i], c.handlers[i+1:]...)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -101,21 +97,33 @@ func (c *HandlerChain) AddHandler(pattern string, handler dns.Handler, priority
|
|||||||
Pattern: pattern,
|
Pattern: pattern,
|
||||||
OrigPattern: origPattern,
|
OrigPattern: origPattern,
|
||||||
IsWildcard: isWildcard,
|
IsWildcard: isWildcard,
|
||||||
StopHandler: stopHandler,
|
|
||||||
MatchSubdomains: matchSubdomains,
|
MatchSubdomains: matchSubdomains,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert handler in priority order
|
pos := c.findHandlerPosition(entry)
|
||||||
pos := 0
|
c.handlers = append(c.handlers[:pos], append([]HandlerEntry{entry}, c.handlers[pos:]...)...)
|
||||||
for i, h := range c.handlers {
|
|
||||||
if h.Priority < priority {
|
|
||||||
pos = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
pos = i + 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.handlers = append(c.handlers[:pos], append([]HandlerEntry{entry}, c.handlers[pos:]...)...)
|
// findHandlerPosition determines where to insert a new handler based on priority and specificity
|
||||||
|
func (c *HandlerChain) findHandlerPosition(newEntry HandlerEntry) int {
|
||||||
|
for i, h := range c.handlers {
|
||||||
|
// prio first
|
||||||
|
if h.Priority < newEntry.Priority {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
// domain specificity next
|
||||||
|
if h.Priority == newEntry.Priority {
|
||||||
|
newDots := strings.Count(newEntry.Pattern, ".")
|
||||||
|
existingDots := strings.Count(h.Pattern, ".")
|
||||||
|
if newDots > existingDots {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add at end
|
||||||
|
return len(c.handlers)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveHandler removes a handler for the given pattern and priority
|
// RemoveHandler removes a handler for the given pattern and priority
|
||||||
@@ -129,9 +137,6 @@ func (c *HandlerChain) RemoveHandler(pattern string, priority int) {
|
|||||||
for i := len(c.handlers) - 1; i >= 0; i-- {
|
for i := len(c.handlers) - 1; i >= 0; i-- {
|
||||||
entry := c.handlers[i]
|
entry := c.handlers[i]
|
||||||
if strings.EqualFold(entry.OrigPattern, pattern) && entry.Priority == priority {
|
if strings.EqualFold(entry.OrigPattern, pattern) && entry.Priority == priority {
|
||||||
if entry.StopHandler != nil {
|
|
||||||
entry.StopHandler.stop()
|
|
||||||
}
|
|
||||||
c.handlers = append(c.handlers[:i], c.handlers[i+1:]...)
|
c.handlers = append(c.handlers[:i], c.handlers[i+1:]...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -167,8 +172,8 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
if log.IsLevelEnabled(log.TraceLevel) {
|
if log.IsLevelEnabled(log.TraceLevel) {
|
||||||
log.Tracef("current handlers (%d):", len(handlers))
|
log.Tracef("current handlers (%d):", len(handlers))
|
||||||
for _, h := range handlers {
|
for _, h := range handlers {
|
||||||
log.Tracef(" - pattern: domain=%s original: domain=%s wildcard=%v priority=%d",
|
log.Tracef(" - pattern: domain=%s original: domain=%s wildcard=%v match_subdomain=%v priority=%d",
|
||||||
h.Pattern, h.OrigPattern, h.IsWildcard, h.Priority)
|
h.Pattern, h.OrigPattern, h.IsWildcard, h.MatchSubdomains, h.Priority)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,13 +198,13 @@ func (c *HandlerChain) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !matched {
|
if !matched {
|
||||||
log.Tracef("trying domain match: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v matched=false",
|
log.Tracef("trying domain match: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v priority=%d matched=false",
|
||||||
qname, entry.OrigPattern, entry.MatchSubdomains, entry.IsWildcard)
|
qname, entry.OrigPattern, entry.MatchSubdomains, entry.IsWildcard, entry.Priority)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Tracef("handler matched: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v",
|
log.Tracef("handler matched: request: domain=%s pattern: domain=%s wildcard=%v match_subdomain=%v priority=%d",
|
||||||
qname, entry.OrigPattern, entry.IsWildcard, entry.MatchSubdomains)
|
qname, entry.OrigPattern, entry.IsWildcard, entry.MatchSubdomains, entry.Priority)
|
||||||
|
|
||||||
chainWriter := &ResponseWriterChain{
|
chainWriter := &ResponseWriterChain{
|
||||||
ResponseWriter: w,
|
ResponseWriter: w,
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ func TestHandlerChain_ServeDNS_Priorities(t *testing.T) {
|
|||||||
dnsRouteHandler := &nbdns.MockHandler{}
|
dnsRouteHandler := &nbdns.MockHandler{}
|
||||||
|
|
||||||
// Setup handlers with different priorities
|
// Setup handlers with different priorities
|
||||||
chain.AddHandler("example.com.", defaultHandler, nbdns.PriorityDefault, nil)
|
chain.AddHandler("example.com.", defaultHandler, nbdns.PriorityDefault)
|
||||||
chain.AddHandler("example.com.", matchDomainHandler, nbdns.PriorityMatchDomain, nil)
|
chain.AddHandler("example.com.", matchDomainHandler, nbdns.PriorityMatchDomain)
|
||||||
chain.AddHandler("example.com.", dnsRouteHandler, nbdns.PriorityDNSRoute, nil)
|
chain.AddHandler("example.com.", dnsRouteHandler, nbdns.PriorityDNSRoute)
|
||||||
|
|
||||||
// Create test request
|
// Create test request
|
||||||
r := new(dns.Msg)
|
r := new(dns.Msg)
|
||||||
@@ -138,7 +138,7 @@ func TestHandlerChain_ServeDNS_DomainMatching(t *testing.T) {
|
|||||||
pattern = "*." + tt.handlerDomain[2:]
|
pattern = "*." + tt.handlerDomain[2:]
|
||||||
}
|
}
|
||||||
|
|
||||||
chain.AddHandler(pattern, handler, nbdns.PriorityDefault, nil)
|
chain.AddHandler(pattern, handler, nbdns.PriorityDefault)
|
||||||
|
|
||||||
r := new(dns.Msg)
|
r := new(dns.Msg)
|
||||||
r.SetQuestion(tt.queryDomain, dns.TypeA)
|
r.SetQuestion(tt.queryDomain, dns.TypeA)
|
||||||
@@ -253,7 +253,7 @@ func TestHandlerChain_ServeDNS_OverlappingDomains(t *testing.T) {
|
|||||||
handler.On("ServeDNS", mock.Anything, mock.Anything).Maybe()
|
handler.On("ServeDNS", mock.Anything, mock.Anything).Maybe()
|
||||||
}
|
}
|
||||||
|
|
||||||
chain.AddHandler(tt.handlers[i].pattern, handler, tt.handlers[i].priority, nil)
|
chain.AddHandler(tt.handlers[i].pattern, handler, tt.handlers[i].priority)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and execute request
|
// Create and execute request
|
||||||
@@ -280,9 +280,9 @@ func TestHandlerChain_ServeDNS_ChainContinuation(t *testing.T) {
|
|||||||
handler3 := &nbdns.MockHandler{}
|
handler3 := &nbdns.MockHandler{}
|
||||||
|
|
||||||
// Add handlers in priority order
|
// Add handlers in priority order
|
||||||
chain.AddHandler("example.com.", handler1, nbdns.PriorityDNSRoute, nil)
|
chain.AddHandler("example.com.", handler1, nbdns.PriorityDNSRoute)
|
||||||
chain.AddHandler("example.com.", handler2, nbdns.PriorityMatchDomain, nil)
|
chain.AddHandler("example.com.", handler2, nbdns.PriorityMatchDomain)
|
||||||
chain.AddHandler("example.com.", handler3, nbdns.PriorityDefault, nil)
|
chain.AddHandler("example.com.", handler3, nbdns.PriorityDefault)
|
||||||
|
|
||||||
// Create test request
|
// Create test request
|
||||||
r := new(dns.Msg)
|
r := new(dns.Msg)
|
||||||
@@ -416,7 +416,7 @@ func TestHandlerChain_PriorityDeregistration(t *testing.T) {
|
|||||||
if op.action == "add" {
|
if op.action == "add" {
|
||||||
handler := &nbdns.MockHandler{}
|
handler := &nbdns.MockHandler{}
|
||||||
handlers[op.priority] = handler
|
handlers[op.priority] = handler
|
||||||
chain.AddHandler(op.pattern, handler, op.priority, nil)
|
chain.AddHandler(op.pattern, handler, op.priority)
|
||||||
} else {
|
} else {
|
||||||
chain.RemoveHandler(op.pattern, op.priority)
|
chain.RemoveHandler(op.pattern, op.priority)
|
||||||
}
|
}
|
||||||
@@ -471,9 +471,9 @@ func TestHandlerChain_MultiPriorityHandling(t *testing.T) {
|
|||||||
r.SetQuestion(testQuery, dns.TypeA)
|
r.SetQuestion(testQuery, dns.TypeA)
|
||||||
|
|
||||||
// Add handlers in mixed order
|
// Add handlers in mixed order
|
||||||
chain.AddHandler(testDomain, defaultHandler, nbdns.PriorityDefault, nil)
|
chain.AddHandler(testDomain, defaultHandler, nbdns.PriorityDefault)
|
||||||
chain.AddHandler(testDomain, routeHandler, nbdns.PriorityDNSRoute, nil)
|
chain.AddHandler(testDomain, routeHandler, nbdns.PriorityDNSRoute)
|
||||||
chain.AddHandler(testDomain, matchHandler, nbdns.PriorityMatchDomain, nil)
|
chain.AddHandler(testDomain, matchHandler, nbdns.PriorityMatchDomain)
|
||||||
|
|
||||||
// Test 1: Initial state with all three handlers
|
// Test 1: Initial state with all three handlers
|
||||||
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
||||||
@@ -653,7 +653,7 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) {
|
|||||||
handler = mockHandler
|
handler = mockHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
chain.AddHandler(pattern, handler, h.priority, nil)
|
chain.AddHandler(pattern, handler, h.priority)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute request
|
// Execute request
|
||||||
@@ -677,3 +677,156 @@ func TestHandlerChain_CaseSensitivity(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandlerChain_DomainSpecificityOrdering(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
scenario string
|
||||||
|
ops []struct {
|
||||||
|
action string
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
subdomain bool
|
||||||
|
}
|
||||||
|
query string
|
||||||
|
expectedMatch string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "more specific domain matches first",
|
||||||
|
scenario: "sub.example.com should match before example.com",
|
||||||
|
ops: []struct {
|
||||||
|
action string
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
subdomain bool
|
||||||
|
}{
|
||||||
|
{"add", "example.com.", nbdns.PriorityMatchDomain, true},
|
||||||
|
{"add", "sub.example.com.", nbdns.PriorityMatchDomain, false},
|
||||||
|
},
|
||||||
|
query: "sub.example.com.",
|
||||||
|
expectedMatch: "sub.example.com.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "more specific domain matches first, both match subdomains",
|
||||||
|
scenario: "sub.example.com should match before example.com",
|
||||||
|
ops: []struct {
|
||||||
|
action string
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
subdomain bool
|
||||||
|
}{
|
||||||
|
{"add", "example.com.", nbdns.PriorityMatchDomain, true},
|
||||||
|
{"add", "sub.example.com.", nbdns.PriorityMatchDomain, true},
|
||||||
|
},
|
||||||
|
query: "sub.example.com.",
|
||||||
|
expectedMatch: "sub.example.com.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "maintain specificity order after removal",
|
||||||
|
scenario: "after removing most specific, should fall back to less specific",
|
||||||
|
ops: []struct {
|
||||||
|
action string
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
subdomain bool
|
||||||
|
}{
|
||||||
|
{"add", "example.com.", nbdns.PriorityMatchDomain, true},
|
||||||
|
{"add", "sub.example.com.", nbdns.PriorityMatchDomain, true},
|
||||||
|
{"add", "test.sub.example.com.", nbdns.PriorityMatchDomain, false},
|
||||||
|
{"remove", "test.sub.example.com.", nbdns.PriorityMatchDomain, false},
|
||||||
|
},
|
||||||
|
query: "test.sub.example.com.",
|
||||||
|
expectedMatch: "sub.example.com.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "priority overrides specificity",
|
||||||
|
scenario: "less specific domain with higher priority should match first",
|
||||||
|
ops: []struct {
|
||||||
|
action string
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
subdomain bool
|
||||||
|
}{
|
||||||
|
{"add", "sub.example.com.", nbdns.PriorityMatchDomain, false},
|
||||||
|
{"add", "example.com.", nbdns.PriorityDNSRoute, true},
|
||||||
|
},
|
||||||
|
query: "sub.example.com.",
|
||||||
|
expectedMatch: "example.com.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "equal priority respects specificity",
|
||||||
|
scenario: "with equal priority, more specific domain should match",
|
||||||
|
ops: []struct {
|
||||||
|
action string
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
subdomain bool
|
||||||
|
}{
|
||||||
|
{"add", "example.com.", nbdns.PriorityMatchDomain, true},
|
||||||
|
{"add", "other.example.com.", nbdns.PriorityMatchDomain, true},
|
||||||
|
{"add", "sub.example.com.", nbdns.PriorityMatchDomain, false},
|
||||||
|
},
|
||||||
|
query: "sub.example.com.",
|
||||||
|
expectedMatch: "sub.example.com.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "specific matches before wildcard",
|
||||||
|
scenario: "specific domain should match before wildcard at same priority",
|
||||||
|
ops: []struct {
|
||||||
|
action string
|
||||||
|
pattern string
|
||||||
|
priority int
|
||||||
|
subdomain bool
|
||||||
|
}{
|
||||||
|
{"add", "*.example.com.", nbdns.PriorityDNSRoute, false},
|
||||||
|
{"add", "sub.example.com.", nbdns.PriorityDNSRoute, false},
|
||||||
|
},
|
||||||
|
query: "sub.example.com.",
|
||||||
|
expectedMatch: "sub.example.com.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
chain := nbdns.NewHandlerChain()
|
||||||
|
handlers := make(map[string]*nbdns.MockSubdomainHandler)
|
||||||
|
|
||||||
|
for _, op := range tt.ops {
|
||||||
|
if op.action == "add" {
|
||||||
|
handler := &nbdns.MockSubdomainHandler{Subdomains: op.subdomain}
|
||||||
|
handlers[op.pattern] = handler
|
||||||
|
chain.AddHandler(op.pattern, handler, op.priority)
|
||||||
|
} else {
|
||||||
|
chain.RemoveHandler(op.pattern, op.priority)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r := new(dns.Msg)
|
||||||
|
r.SetQuestion(tt.query, dns.TypeA)
|
||||||
|
w := &nbdns.ResponseWriterChain{ResponseWriter: &mockResponseWriter{}}
|
||||||
|
|
||||||
|
// Setup handler expectations
|
||||||
|
for pattern, handler := range handlers {
|
||||||
|
if pattern == tt.expectedMatch {
|
||||||
|
handler.On("ServeDNS", mock.Anything, r).Run(func(args mock.Arguments) {
|
||||||
|
w := args.Get(0).(dns.ResponseWriter)
|
||||||
|
r := args.Get(1).(*dns.Msg)
|
||||||
|
resp := new(dns.Msg)
|
||||||
|
resp.SetReply(r)
|
||||||
|
assert.NoError(t, w.WriteMsg(resp))
|
||||||
|
}).Once()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.ServeDNS(w, r)
|
||||||
|
|
||||||
|
for pattern, handler := range handlers {
|
||||||
|
if pattern == tt.expectedMatch {
|
||||||
|
handler.AssertNumberOfCalls(t, "ServeDNS", 1)
|
||||||
|
} else {
|
||||||
|
handler.AssertNumberOfCalls(t, "ServeDNS", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,10 +29,15 @@ func (d *localResolver) String() string {
|
|||||||
return fmt.Sprintf("local resolver [%d records]", len(d.registeredMap))
|
return fmt.Sprintf("local resolver [%d records]", len(d.registeredMap))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID returns the unique handler ID
|
||||||
|
func (d *localResolver) id() handlerID {
|
||||||
|
return "local-resolver"
|
||||||
|
}
|
||||||
|
|
||||||
// ServeDNS handles a DNS request
|
// ServeDNS handles a DNS request
|
||||||
func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
func (d *localResolver) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
if len(r.Question) > 0 {
|
if len(r.Question) > 0 {
|
||||||
log.Tracef("received question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass)
|
log.Tracef("received local question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass)
|
||||||
}
|
}
|
||||||
|
|
||||||
replyMessage := &dns.Msg{}
|
replyMessage := &dns.Msg{}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
@@ -42,7 +41,12 @@ type Server interface {
|
|||||||
ProbeAvailability()
|
ProbeAvailability()
|
||||||
}
|
}
|
||||||
|
|
||||||
type registeredHandlerMap map[string]handlerWithStop
|
type handlerID string
|
||||||
|
|
||||||
|
type nsGroupsByDomain struct {
|
||||||
|
domain string
|
||||||
|
groups []*nbdns.NameServerGroup
|
||||||
|
}
|
||||||
|
|
||||||
// DefaultServer dns server object
|
// DefaultServer dns server object
|
||||||
type DefaultServer struct {
|
type DefaultServer struct {
|
||||||
@@ -52,7 +56,6 @@ type DefaultServer struct {
|
|||||||
mux sync.Mutex
|
mux sync.Mutex
|
||||||
service service
|
service service
|
||||||
dnsMuxMap registeredHandlerMap
|
dnsMuxMap registeredHandlerMap
|
||||||
handlerPriorities map[string]int
|
|
||||||
localResolver *localResolver
|
localResolver *localResolver
|
||||||
wgInterface WGIface
|
wgInterface WGIface
|
||||||
hostManager hostManager
|
hostManager hostManager
|
||||||
@@ -77,14 +80,17 @@ type handlerWithStop interface {
|
|||||||
dns.Handler
|
dns.Handler
|
||||||
stop()
|
stop()
|
||||||
probeAvailability()
|
probeAvailability()
|
||||||
|
id() handlerID
|
||||||
}
|
}
|
||||||
|
|
||||||
type muxUpdate struct {
|
type handlerWrapper struct {
|
||||||
domain string
|
domain string
|
||||||
handler handlerWithStop
|
handler handlerWithStop
|
||||||
priority int
|
priority int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type registeredHandlerMap map[handlerID]handlerWrapper
|
||||||
|
|
||||||
// NewDefaultServer returns a new dns server
|
// NewDefaultServer returns a new dns server
|
||||||
func NewDefaultServer(
|
func NewDefaultServer(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
@@ -164,7 +170,6 @@ func newDefaultServer(
|
|||||||
service: dnsService,
|
service: dnsService,
|
||||||
handlerChain: NewHandlerChain(),
|
handlerChain: NewHandlerChain(),
|
||||||
dnsMuxMap: make(registeredHandlerMap),
|
dnsMuxMap: make(registeredHandlerMap),
|
||||||
handlerPriorities: make(map[string]int),
|
|
||||||
localResolver: &localResolver{
|
localResolver: &localResolver{
|
||||||
registeredMap: make(registrationMap),
|
registeredMap: make(registrationMap),
|
||||||
},
|
},
|
||||||
@@ -192,8 +197,7 @@ func (s *DefaultServer) registerHandler(domains []string, handler dns.Handler, p
|
|||||||
log.Warn("skipping empty domain")
|
log.Warn("skipping empty domain")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.handlerChain.AddHandler(domain, handler, priority, nil)
|
s.handlerChain.AddHandler(domain, handler, priority)
|
||||||
s.handlerPriorities[domain] = priority
|
|
||||||
s.service.RegisterMux(nbdns.NormalizeZone(domain), s.handlerChain)
|
s.service.RegisterMux(nbdns.NormalizeZone(domain), s.handlerChain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,14 +213,15 @@ func (s *DefaultServer) deregisterHandler(domains []string, priority int) {
|
|||||||
log.Debugf("deregistering handler %v with priority %d", domains, priority)
|
log.Debugf("deregistering handler %v with priority %d", domains, priority)
|
||||||
|
|
||||||
for _, domain := range domains {
|
for _, domain := range domains {
|
||||||
s.handlerChain.RemoveHandler(domain, priority)
|
|
||||||
|
|
||||||
// Only deregister from service if no handlers remain
|
|
||||||
if !s.handlerChain.HasHandlers(domain) {
|
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
log.Warn("skipping empty domain")
|
log.Warn("skipping empty domain")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.handlerChain.RemoveHandler(domain, priority)
|
||||||
|
|
||||||
|
// Only deregister from service if no handlers remain
|
||||||
|
if !s.handlerChain.HasHandlers(domain) {
|
||||||
s.service.DeregisterMux(nbdns.NormalizeZone(domain))
|
s.service.DeregisterMux(nbdns.NormalizeZone(domain))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,14 +288,24 @@ func (s *DefaultServer) Stop() {
|
|||||||
|
|
||||||
// OnUpdatedHostDNSServer update the DNS servers addresses for root zones
|
// OnUpdatedHostDNSServer update the DNS servers addresses for root zones
|
||||||
// It will be applied if the mgm server do not enforce DNS settings for root zone
|
// It will be applied if the mgm server do not enforce DNS settings for root zone
|
||||||
|
|
||||||
func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []string) {
|
func (s *DefaultServer) OnUpdatedHostDNSServer(hostsDnsList []string) {
|
||||||
s.hostsDNSHolder.set(hostsDnsList)
|
s.hostsDNSHolder.set(hostsDnsList)
|
||||||
|
|
||||||
_, ok := s.dnsMuxMap[nbdns.RootZone]
|
// Check if there's any root handler
|
||||||
if ok {
|
var hasRootHandler bool
|
||||||
|
for _, handler := range s.dnsMuxMap {
|
||||||
|
if handler.domain == nbdns.RootZone {
|
||||||
|
hasRootHandler = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasRootHandler {
|
||||||
log.Debugf("on new host DNS config but skip to apply it")
|
log.Debugf("on new host DNS config but skip to apply it")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("update host DNS settings: %+v", hostsDnsList)
|
log.Debugf("update host DNS settings: %+v", hostsDnsList)
|
||||||
s.addHostRootZone()
|
s.addHostRootZone()
|
||||||
}
|
}
|
||||||
@@ -364,7 +379,7 @@ func (s *DefaultServer) ProbeAvailability() {
|
|||||||
go func(mux handlerWithStop) {
|
go func(mux handlerWithStop) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
mux.probeAvailability()
|
mux.probeAvailability()
|
||||||
}(mux)
|
}(mux.handler)
|
||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
@@ -419,8 +434,8 @@ func (s *DefaultServer) applyConfiguration(update nbdns.Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]muxUpdate, map[string]nbdns.SimpleRecord, error) {
|
func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone) ([]handlerWrapper, map[string]nbdns.SimpleRecord, error) {
|
||||||
var muxUpdates []muxUpdate
|
var muxUpdates []handlerWrapper
|
||||||
localRecords := make(map[string]nbdns.SimpleRecord, 0)
|
localRecords := make(map[string]nbdns.SimpleRecord, 0)
|
||||||
|
|
||||||
for _, customZone := range customZones {
|
for _, customZone := range customZones {
|
||||||
@@ -428,7 +443,7 @@ func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone)
|
|||||||
return nil, nil, fmt.Errorf("received an empty list of records")
|
return nil, nil, fmt.Errorf("received an empty list of records")
|
||||||
}
|
}
|
||||||
|
|
||||||
muxUpdates = append(muxUpdates, muxUpdate{
|
muxUpdates = append(muxUpdates, handlerWrapper{
|
||||||
domain: customZone.Domain,
|
domain: customZone.Domain,
|
||||||
handler: s.localResolver,
|
handler: s.localResolver,
|
||||||
priority: PriorityMatchDomain,
|
priority: PriorityMatchDomain,
|
||||||
@@ -446,15 +461,59 @@ func (s *DefaultServer) buildLocalHandlerUpdate(customZones []nbdns.CustomZone)
|
|||||||
return muxUpdates, localRecords, nil
|
return muxUpdates, localRecords, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.NameServerGroup) ([]muxUpdate, error) {
|
func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.NameServerGroup) ([]handlerWrapper, error) {
|
||||||
|
var muxUpdates []handlerWrapper
|
||||||
|
|
||||||
var muxUpdates []muxUpdate
|
|
||||||
for _, nsGroup := range nameServerGroups {
|
for _, nsGroup := range nameServerGroups {
|
||||||
if len(nsGroup.NameServers) == 0 {
|
if len(nsGroup.NameServers) == 0 {
|
||||||
log.Warn("received a nameserver group with empty nameserver list")
|
log.Warn("received a nameserver group with empty nameserver list")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !nsGroup.Primary && len(nsGroup.Domains) == 0 {
|
||||||
|
return nil, fmt.Errorf("received a non primary nameserver group with an empty domain list")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, domain := range nsGroup.Domains {
|
||||||
|
if domain == "" {
|
||||||
|
return nil, fmt.Errorf("received a nameserver group with an empty domain element")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groupedNS := groupNSGroupsByDomain(nameServerGroups)
|
||||||
|
|
||||||
|
for _, domainGroup := range groupedNS {
|
||||||
|
basePriority := PriorityMatchDomain
|
||||||
|
if domainGroup.domain == nbdns.RootZone {
|
||||||
|
basePriority = PriorityDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
updates, err := s.createHandlersForDomainGroup(domainGroup, basePriority)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
muxUpdates = append(muxUpdates, updates...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return muxUpdates, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DefaultServer) createHandlersForDomainGroup(domainGroup nsGroupsByDomain, basePriority int) ([]handlerWrapper, error) {
|
||||||
|
var muxUpdates []handlerWrapper
|
||||||
|
|
||||||
|
for i, nsGroup := range domainGroup.groups {
|
||||||
|
// Decrement priority by handler index (0, 1, 2, ...) to avoid conflicts
|
||||||
|
priority := basePriority - i
|
||||||
|
|
||||||
|
// Check if we're about to overlap with the next priority tier
|
||||||
|
if basePriority == PriorityMatchDomain && priority <= PriorityDefault {
|
||||||
|
log.Warnf("too many handlers for domain=%s, would overlap with default priority tier (diff=%d). Skipping remaining handlers",
|
||||||
|
domainGroup.domain, PriorityMatchDomain-PriorityDefault)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("creating handler for domain=%s with priority=%d", domainGroup.domain, priority)
|
||||||
handler, err := newUpstreamResolver(
|
handler, err := newUpstreamResolver(
|
||||||
s.ctx,
|
s.ctx,
|
||||||
s.wgInterface.Name(),
|
s.wgInterface.Name(),
|
||||||
@@ -462,10 +521,12 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam
|
|||||||
s.wgInterface.Address().Network,
|
s.wgInterface.Address().Network,
|
||||||
s.statusRecorder,
|
s.statusRecorder,
|
||||||
s.hostsDNSHolder,
|
s.hostsDNSHolder,
|
||||||
|
domainGroup.domain,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to create a new upstream resolver, error: %v", err)
|
return nil, fmt.Errorf("create upstream resolver: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, ns := range nsGroup.NameServers {
|
for _, ns := range nsGroup.NameServers {
|
||||||
if ns.NSType != nbdns.UDPNameServerType {
|
if ns.NSType != nbdns.UDPNameServerType {
|
||||||
log.Warnf("skipping nameserver %s with type %s, this peer supports only %s",
|
log.Warnf("skipping nameserver %s with type %s, this peer supports only %s",
|
||||||
@@ -489,78 +550,47 @@ func (s *DefaultServer) buildUpstreamHandlerUpdate(nameServerGroups []*nbdns.Nam
|
|||||||
// after some period defined by upstream it tries to reactivate self by calling this hook
|
// after some period defined by upstream it tries to reactivate self by calling this hook
|
||||||
// everything we need here is just to re-apply current configuration because it already
|
// everything we need here is just to re-apply current configuration because it already
|
||||||
// contains this upstream settings (temporal deactivation not removed it)
|
// contains this upstream settings (temporal deactivation not removed it)
|
||||||
handler.deactivate, handler.reactivate = s.upstreamCallbacks(nsGroup, handler)
|
handler.deactivate, handler.reactivate = s.upstreamCallbacks(nsGroup, handler, priority)
|
||||||
|
|
||||||
if nsGroup.Primary {
|
muxUpdates = append(muxUpdates, handlerWrapper{
|
||||||
muxUpdates = append(muxUpdates, muxUpdate{
|
domain: domainGroup.domain,
|
||||||
domain: nbdns.RootZone,
|
|
||||||
handler: handler,
|
handler: handler,
|
||||||
priority: PriorityDefault,
|
priority: priority,
|
||||||
})
|
})
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(nsGroup.Domains) == 0 {
|
|
||||||
handler.stop()
|
|
||||||
return nil, fmt.Errorf("received a non primary nameserver group with an empty domain list")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, domain := range nsGroup.Domains {
|
|
||||||
if domain == "" {
|
|
||||||
handler.stop()
|
|
||||||
return nil, fmt.Errorf("received a nameserver group with an empty domain element")
|
|
||||||
}
|
|
||||||
muxUpdates = append(muxUpdates, muxUpdate{
|
|
||||||
domain: domain,
|
|
||||||
handler: handler,
|
|
||||||
priority: PriorityMatchDomain,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return muxUpdates, nil
|
return muxUpdates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) updateMux(muxUpdates []muxUpdate) {
|
func (s *DefaultServer) updateMux(muxUpdates []handlerWrapper) {
|
||||||
|
// this will introduce a short period of time when the server is not able to handle DNS requests
|
||||||
|
for _, existing := range s.dnsMuxMap {
|
||||||
|
s.deregisterHandler([]string{existing.domain}, existing.priority)
|
||||||
|
existing.handler.stop()
|
||||||
|
}
|
||||||
|
|
||||||
muxUpdateMap := make(registeredHandlerMap)
|
muxUpdateMap := make(registeredHandlerMap)
|
||||||
handlersByPriority := make(map[string]int)
|
var containsRootUpdate bool
|
||||||
|
|
||||||
var isContainRootUpdate bool
|
|
||||||
|
|
||||||
// First register new handlers
|
|
||||||
for _, update := range muxUpdates {
|
for _, update := range muxUpdates {
|
||||||
s.registerHandler([]string{update.domain}, update.handler, update.priority)
|
|
||||||
muxUpdateMap[update.domain] = update.handler
|
|
||||||
handlersByPriority[update.domain] = update.priority
|
|
||||||
|
|
||||||
if existingHandler, ok := s.dnsMuxMap[update.domain]; ok {
|
|
||||||
existingHandler.stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
if update.domain == nbdns.RootZone {
|
if update.domain == nbdns.RootZone {
|
||||||
isContainRootUpdate = true
|
containsRootUpdate = true
|
||||||
}
|
}
|
||||||
|
s.registerHandler([]string{update.domain}, update.handler, update.priority)
|
||||||
|
muxUpdateMap[update.handler.id()] = update
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then deregister old handlers not in the update
|
// If there's no root update and we had a root handler, restore it
|
||||||
for key, existingHandler := range s.dnsMuxMap {
|
if !containsRootUpdate {
|
||||||
_, found := muxUpdateMap[key]
|
for _, existing := range s.dnsMuxMap {
|
||||||
if !found {
|
if existing.domain == nbdns.RootZone {
|
||||||
if !isContainRootUpdate && key == nbdns.RootZone {
|
|
||||||
s.addHostRootZone()
|
s.addHostRootZone()
|
||||||
existingHandler.stop()
|
break
|
||||||
} else {
|
|
||||||
existingHandler.stop()
|
|
||||||
// Deregister with the priority that was used to register
|
|
||||||
if oldPriority, ok := s.handlerPriorities[key]; ok {
|
|
||||||
s.deregisterHandler([]string{key}, oldPriority)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.dnsMuxMap = muxUpdateMap
|
s.dnsMuxMap = muxUpdateMap
|
||||||
s.handlerPriorities = handlersByPriority
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord) {
|
func (s *DefaultServer) updateLocalResolver(update map[string]nbdns.SimpleRecord) {
|
||||||
@@ -593,6 +623,7 @@ func getNSHostPort(ns nbdns.NameServer) string {
|
|||||||
func (s *DefaultServer) upstreamCallbacks(
|
func (s *DefaultServer) upstreamCallbacks(
|
||||||
nsGroup *nbdns.NameServerGroup,
|
nsGroup *nbdns.NameServerGroup,
|
||||||
handler dns.Handler,
|
handler dns.Handler,
|
||||||
|
priority int,
|
||||||
) (deactivate func(error), reactivate func()) {
|
) (deactivate func(error), reactivate func()) {
|
||||||
var removeIndex map[string]int
|
var removeIndex map[string]int
|
||||||
deactivate = func(err error) {
|
deactivate = func(err error) {
|
||||||
@@ -609,13 +640,13 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
if nsGroup.Primary {
|
if nsGroup.Primary {
|
||||||
removeIndex[nbdns.RootZone] = -1
|
removeIndex[nbdns.RootZone] = -1
|
||||||
s.currentConfig.RouteAll = false
|
s.currentConfig.RouteAll = false
|
||||||
s.deregisterHandler([]string{nbdns.RootZone}, PriorityDefault)
|
s.deregisterHandler([]string{nbdns.RootZone}, priority)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, item := range s.currentConfig.Domains {
|
for i, item := range s.currentConfig.Domains {
|
||||||
if _, found := removeIndex[item.Domain]; found {
|
if _, found := removeIndex[item.Domain]; found {
|
||||||
s.currentConfig.Domains[i].Disabled = true
|
s.currentConfig.Domains[i].Disabled = true
|
||||||
s.deregisterHandler([]string{item.Domain}, PriorityMatchDomain)
|
s.deregisterHandler([]string{item.Domain}, priority)
|
||||||
removeIndex[item.Domain] = i
|
removeIndex[item.Domain] = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -635,8 +666,8 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.updateNSState(nsGroup, err, false)
|
s.updateNSState(nsGroup, err, false)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reactivate = func() {
|
reactivate = func() {
|
||||||
s.mux.Lock()
|
s.mux.Lock()
|
||||||
defer s.mux.Unlock()
|
defer s.mux.Unlock()
|
||||||
@@ -646,7 +677,7 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
s.currentConfig.Domains[i].Disabled = false
|
s.currentConfig.Domains[i].Disabled = false
|
||||||
s.registerHandler([]string{domain}, handler, PriorityMatchDomain)
|
s.registerHandler([]string{domain}, handler, priority)
|
||||||
}
|
}
|
||||||
|
|
||||||
l := log.WithField("nameservers", nsGroup.NameServers)
|
l := log.WithField("nameservers", nsGroup.NameServers)
|
||||||
@@ -654,7 +685,7 @@ func (s *DefaultServer) upstreamCallbacks(
|
|||||||
|
|
||||||
if nsGroup.Primary {
|
if nsGroup.Primary {
|
||||||
s.currentConfig.RouteAll = true
|
s.currentConfig.RouteAll = true
|
||||||
s.registerHandler([]string{nbdns.RootZone}, handler, PriorityDefault)
|
s.registerHandler([]string{nbdns.RootZone}, handler, priority)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.hostManager != nil {
|
if s.hostManager != nil {
|
||||||
@@ -676,6 +707,7 @@ func (s *DefaultServer) addHostRootZone() {
|
|||||||
s.wgInterface.Address().Network,
|
s.wgInterface.Address().Network,
|
||||||
s.statusRecorder,
|
s.statusRecorder,
|
||||||
s.hostsDNSHolder,
|
s.hostsDNSHolder,
|
||||||
|
nbdns.RootZone,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("unable to create a new upstream resolver, error: %v", err)
|
log.Errorf("unable to create a new upstream resolver, error: %v", err)
|
||||||
@@ -732,5 +764,34 @@ func generateGroupKey(nsGroup *nbdns.NameServerGroup) string {
|
|||||||
for _, ns := range nsGroup.NameServers {
|
for _, ns := range nsGroup.NameServers {
|
||||||
servers = append(servers, fmt.Sprintf("%s:%d", ns.IP, ns.Port))
|
servers = append(servers, fmt.Sprintf("%s:%d", ns.IP, ns.Port))
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s_%s_%s", nsGroup.ID, nsGroup.Name, strings.Join(servers, ","))
|
return fmt.Sprintf("%v_%v", servers, nsGroup.Domains)
|
||||||
|
}
|
||||||
|
|
||||||
|
// groupNSGroupsByDomain groups nameserver groups by their match domains
|
||||||
|
func groupNSGroupsByDomain(nsGroups []*nbdns.NameServerGroup) []nsGroupsByDomain {
|
||||||
|
domainMap := make(map[string][]*nbdns.NameServerGroup)
|
||||||
|
|
||||||
|
for _, group := range nsGroups {
|
||||||
|
if group.Primary {
|
||||||
|
domainMap[nbdns.RootZone] = append(domainMap[nbdns.RootZone], group)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, domain := range group.Domains {
|
||||||
|
if domain == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
domainMap[domain] = append(domainMap[domain], group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []nsGroupsByDomain
|
||||||
|
for domain, groups := range domainMap {
|
||||||
|
result = append(result, nsGroupsByDomain{
|
||||||
|
domain: domain,
|
||||||
|
groups: groups,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/golang/mock/gomock"
|
"github.com/golang/mock/gomock"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
|
||||||
@@ -88,6 +89,18 @@ func init() {
|
|||||||
formatter.SetTextFormatter(log.StandardLogger())
|
formatter.SetTextFormatter(log.StandardLogger())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateDummyHandler(domain string, servers []nbdns.NameServer) *upstreamResolverBase {
|
||||||
|
var srvs []string
|
||||||
|
for _, srv := range servers {
|
||||||
|
srvs = append(srvs, getNSHostPort(srv))
|
||||||
|
}
|
||||||
|
return &upstreamResolverBase{
|
||||||
|
domain: domain,
|
||||||
|
upstreamServers: srvs,
|
||||||
|
cancel: func() {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUpdateDNSServer(t *testing.T) {
|
func TestUpdateDNSServer(t *testing.T) {
|
||||||
nameServers := []nbdns.NameServer{
|
nameServers := []nbdns.NameServer{
|
||||||
{
|
{
|
||||||
@@ -140,13 +153,35 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedUpstreamMap: registeredHandlerMap{"netbird.io": dummyHandler, "netbird.cloud": dummyHandler, nbdns.RootZone: dummyHandler},
|
expectedUpstreamMap: registeredHandlerMap{
|
||||||
|
generateDummyHandler("netbird.io", nameServers).id(): handlerWrapper{
|
||||||
|
domain: "netbird.io",
|
||||||
|
handler: dummyHandler,
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
dummyHandler.id(): handlerWrapper{
|
||||||
|
domain: "netbird.cloud",
|
||||||
|
handler: dummyHandler,
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
generateDummyHandler(".", nameServers).id(): handlerWrapper{
|
||||||
|
domain: nbdns.RootZone,
|
||||||
|
handler: dummyHandler,
|
||||||
|
priority: PriorityDefault,
|
||||||
|
},
|
||||||
|
},
|
||||||
expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
|
expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "New Config Should Succeed",
|
name: "New Config Should Succeed",
|
||||||
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
|
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
|
||||||
initUpstreamMap: registeredHandlerMap{buildRecordKey(zoneRecords[0].Name, 1, 1): dummyHandler},
|
initUpstreamMap: registeredHandlerMap{
|
||||||
|
generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{
|
||||||
|
domain: buildRecordKey(zoneRecords[0].Name, 1, 1),
|
||||||
|
handler: dummyHandler,
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
},
|
||||||
initSerial: 0,
|
initSerial: 0,
|
||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
inputUpdate: nbdns.Config{
|
inputUpdate: nbdns.Config{
|
||||||
@@ -164,7 +199,18 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedUpstreamMap: registeredHandlerMap{"netbird.io": dummyHandler, "netbird.cloud": dummyHandler},
|
expectedUpstreamMap: registeredHandlerMap{
|
||||||
|
generateDummyHandler("netbird.io", nameServers).id(): handlerWrapper{
|
||||||
|
domain: "netbird.io",
|
||||||
|
handler: dummyHandler,
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
"local-resolver": handlerWrapper{
|
||||||
|
domain: "netbird.cloud",
|
||||||
|
handler: dummyHandler,
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
},
|
||||||
expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
|
expectedLocalMap: registrationMap{buildRecordKey(zoneRecords[0].Name, 1, 1): struct{}{}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -244,7 +290,13 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Empty Config Should Succeed and Clean Maps",
|
name: "Empty Config Should Succeed and Clean Maps",
|
||||||
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
|
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
|
||||||
initUpstreamMap: registeredHandlerMap{zoneRecords[0].Name: dummyHandler},
|
initUpstreamMap: registeredHandlerMap{
|
||||||
|
generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{
|
||||||
|
domain: zoneRecords[0].Name,
|
||||||
|
handler: dummyHandler,
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
},
|
||||||
initSerial: 0,
|
initSerial: 0,
|
||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
inputUpdate: nbdns.Config{ServiceEnable: true},
|
inputUpdate: nbdns.Config{ServiceEnable: true},
|
||||||
@@ -254,7 +306,13 @@ func TestUpdateDNSServer(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Disabled Service Should clean map",
|
name: "Disabled Service Should clean map",
|
||||||
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
|
initLocalMap: registrationMap{"netbird.cloud": struct{}{}},
|
||||||
initUpstreamMap: registeredHandlerMap{zoneRecords[0].Name: dummyHandler},
|
initUpstreamMap: registeredHandlerMap{
|
||||||
|
generateDummyHandler(zoneRecords[0].Name, nameServers).id(): handlerWrapper{
|
||||||
|
domain: zoneRecords[0].Name,
|
||||||
|
handler: dummyHandler,
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
},
|
||||||
initSerial: 0,
|
initSerial: 0,
|
||||||
inputSerial: 1,
|
inputSerial: 1,
|
||||||
inputUpdate: nbdns.Config{ServiceEnable: false},
|
inputUpdate: nbdns.Config{ServiceEnable: false},
|
||||||
@@ -421,7 +479,13 @@ func TestDNSFakeResolverHandleUpdates(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
dnsServer.dnsMuxMap = registeredHandlerMap{zoneRecords[0].Name: &localResolver{}}
|
dnsServer.dnsMuxMap = registeredHandlerMap{
|
||||||
|
"id1": handlerWrapper{
|
||||||
|
domain: zoneRecords[0].Name,
|
||||||
|
handler: &localResolver{},
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
}
|
||||||
dnsServer.localResolver.registeredMap = registrationMap{"netbird.cloud": struct{}{}}
|
dnsServer.localResolver.registeredMap = registrationMap{"netbird.cloud": struct{}{}}
|
||||||
dnsServer.updateSerial = 0
|
dnsServer.updateSerial = 0
|
||||||
|
|
||||||
@@ -563,7 +627,6 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
|
|||||||
registeredMap: make(registrationMap),
|
registeredMap: make(registrationMap),
|
||||||
},
|
},
|
||||||
handlerChain: NewHandlerChain(),
|
handlerChain: NewHandlerChain(),
|
||||||
handlerPriorities: make(map[string]int),
|
|
||||||
hostManager: hostManager,
|
hostManager: hostManager,
|
||||||
currentConfig: HostDNSConfig{
|
currentConfig: HostDNSConfig{
|
||||||
Domains: []DomainConfig{
|
Domains: []DomainConfig{
|
||||||
@@ -593,7 +656,7 @@ func TestDNSServerUpstreamDeactivateCallback(t *testing.T) {
|
|||||||
NameServers: []nbdns.NameServer{
|
NameServers: []nbdns.NameServer{
|
||||||
{IP: netip.MustParseAddr("8.8.0.0"), NSType: nbdns.UDPNameServerType, Port: 53},
|
{IP: netip.MustParseAddr("8.8.0.0"), NSType: nbdns.UDPNameServerType, Port: 53},
|
||||||
},
|
},
|
||||||
}, nil)
|
}, nil, 0)
|
||||||
|
|
||||||
deactivate(nil)
|
deactivate(nil)
|
||||||
expected := "domain0,domain2"
|
expected := "domain0,domain2"
|
||||||
@@ -849,7 +912,7 @@ func createWgInterfaceWithBind(t *testing.T) (*iface.WGIface, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pf, err := uspfilter.Create(wgIface)
|
pf, err := uspfilter.Create(wgIface, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to create uspfilter: %v", err)
|
t.Fatalf("failed to create uspfilter: %v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -903,8 +966,8 @@ func TestHandlerChain_DomainPriorities(t *testing.T) {
|
|||||||
Subdomains: true,
|
Subdomains: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
chain.AddHandler("example.com.", dnsRouteHandler, PriorityDNSRoute, nil)
|
chain.AddHandler("example.com.", dnsRouteHandler, PriorityDNSRoute)
|
||||||
chain.AddHandler("example.com.", upstreamHandler, PriorityMatchDomain, nil)
|
chain.AddHandler("example.com.", upstreamHandler, PriorityMatchDomain)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -959,3 +1022,421 @@ func TestHandlerChain_DomainPriorities(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockHandler struct {
|
||||||
|
Id string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockHandler) ServeDNS(dns.ResponseWriter, *dns.Msg) {}
|
||||||
|
func (m *mockHandler) stop() {}
|
||||||
|
func (m *mockHandler) probeAvailability() {}
|
||||||
|
func (m *mockHandler) id() handlerID { return handlerID(m.Id) }
|
||||||
|
|
||||||
|
type mockService struct{}
|
||||||
|
|
||||||
|
func (m *mockService) Listen() error { return nil }
|
||||||
|
func (m *mockService) Stop() {}
|
||||||
|
func (m *mockService) RuntimeIP() string { return "127.0.0.1" }
|
||||||
|
func (m *mockService) RuntimePort() int { return 53 }
|
||||||
|
func (m *mockService) RegisterMux(string, dns.Handler) {}
|
||||||
|
func (m *mockService) DeregisterMux(string) {}
|
||||||
|
|
||||||
|
func TestDefaultServer_UpdateMux(t *testing.T) {
|
||||||
|
baseMatchHandlers := registeredHandlerMap{
|
||||||
|
"upstream-group1": {
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group1",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
"upstream-group2": {
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group2",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain - 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
baseRootHandlers := registeredHandlerMap{
|
||||||
|
"upstream-root1": {
|
||||||
|
domain: ".",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-root1",
|
||||||
|
},
|
||||||
|
priority: PriorityDefault,
|
||||||
|
},
|
||||||
|
"upstream-root2": {
|
||||||
|
domain: ".",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-root2",
|
||||||
|
},
|
||||||
|
priority: PriorityDefault - 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
baseMixedHandlers := registeredHandlerMap{
|
||||||
|
"upstream-group1": {
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group1",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
"upstream-group2": {
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group2",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain - 1,
|
||||||
|
},
|
||||||
|
"upstream-other": {
|
||||||
|
domain: "other.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-other",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
initialHandlers registeredHandlerMap
|
||||||
|
updates []handlerWrapper
|
||||||
|
expectedHandlers map[string]string // map[handlerID]domain
|
||||||
|
description string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Remove group1 from update",
|
||||||
|
initialHandlers: baseMatchHandlers,
|
||||||
|
updates: []handlerWrapper{
|
||||||
|
// Only group2 remains
|
||||||
|
{
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group2",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain - 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedHandlers: map[string]string{
|
||||||
|
"upstream-group2": "example.com",
|
||||||
|
},
|
||||||
|
description: "When group1 is not included in the update, it should be removed while group2 remains",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Remove group2 from update",
|
||||||
|
initialHandlers: baseMatchHandlers,
|
||||||
|
updates: []handlerWrapper{
|
||||||
|
// Only group1 remains
|
||||||
|
{
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group1",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedHandlers: map[string]string{
|
||||||
|
"upstream-group1": "example.com",
|
||||||
|
},
|
||||||
|
description: "When group2 is not included in the update, it should be removed while group1 remains",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Add group3 in first position",
|
||||||
|
initialHandlers: baseMatchHandlers,
|
||||||
|
updates: []handlerWrapper{
|
||||||
|
// Add group3 with highest priority
|
||||||
|
{
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group3",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain + 1,
|
||||||
|
},
|
||||||
|
// Keep existing groups with their original priorities
|
||||||
|
{
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group1",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group2",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain - 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedHandlers: map[string]string{
|
||||||
|
"upstream-group1": "example.com",
|
||||||
|
"upstream-group2": "example.com",
|
||||||
|
"upstream-group3": "example.com",
|
||||||
|
},
|
||||||
|
description: "When adding group3 with highest priority, it should be first in chain while maintaining existing groups",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Add group3 in last position",
|
||||||
|
initialHandlers: baseMatchHandlers,
|
||||||
|
updates: []handlerWrapper{
|
||||||
|
// Keep existing groups with their original priorities
|
||||||
|
{
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group1",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group2",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain - 1,
|
||||||
|
},
|
||||||
|
// Add group3 with lowest priority
|
||||||
|
{
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group3",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain - 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedHandlers: map[string]string{
|
||||||
|
"upstream-group1": "example.com",
|
||||||
|
"upstream-group2": "example.com",
|
||||||
|
"upstream-group3": "example.com",
|
||||||
|
},
|
||||||
|
description: "When adding group3 with lowest priority, it should be last in chain while maintaining existing groups",
|
||||||
|
},
|
||||||
|
// Root zone tests
|
||||||
|
{
|
||||||
|
name: "Remove root1 from update",
|
||||||
|
initialHandlers: baseRootHandlers,
|
||||||
|
updates: []handlerWrapper{
|
||||||
|
{
|
||||||
|
domain: ".",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-root2",
|
||||||
|
},
|
||||||
|
priority: PriorityDefault - 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedHandlers: map[string]string{
|
||||||
|
"upstream-root2": ".",
|
||||||
|
},
|
||||||
|
description: "When root1 is not included in the update, it should be removed while root2 remains",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Remove root2 from update",
|
||||||
|
initialHandlers: baseRootHandlers,
|
||||||
|
updates: []handlerWrapper{
|
||||||
|
{
|
||||||
|
domain: ".",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-root1",
|
||||||
|
},
|
||||||
|
priority: PriorityDefault,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedHandlers: map[string]string{
|
||||||
|
"upstream-root1": ".",
|
||||||
|
},
|
||||||
|
description: "When root2 is not included in the update, it should be removed while root1 remains",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Add root3 in first position",
|
||||||
|
initialHandlers: baseRootHandlers,
|
||||||
|
updates: []handlerWrapper{
|
||||||
|
{
|
||||||
|
domain: ".",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-root3",
|
||||||
|
},
|
||||||
|
priority: PriorityDefault + 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: ".",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-root1",
|
||||||
|
},
|
||||||
|
priority: PriorityDefault,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: ".",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-root2",
|
||||||
|
},
|
||||||
|
priority: PriorityDefault - 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedHandlers: map[string]string{
|
||||||
|
"upstream-root1": ".",
|
||||||
|
"upstream-root2": ".",
|
||||||
|
"upstream-root3": ".",
|
||||||
|
},
|
||||||
|
description: "When adding root3 with highest priority, it should be first in chain while maintaining existing root handlers",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Add root3 in last position",
|
||||||
|
initialHandlers: baseRootHandlers,
|
||||||
|
updates: []handlerWrapper{
|
||||||
|
{
|
||||||
|
domain: ".",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-root1",
|
||||||
|
},
|
||||||
|
priority: PriorityDefault,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: ".",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-root2",
|
||||||
|
},
|
||||||
|
priority: PriorityDefault - 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: ".",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-root3",
|
||||||
|
},
|
||||||
|
priority: PriorityDefault - 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedHandlers: map[string]string{
|
||||||
|
"upstream-root1": ".",
|
||||||
|
"upstream-root2": ".",
|
||||||
|
"upstream-root3": ".",
|
||||||
|
},
|
||||||
|
description: "When adding root3 with lowest priority, it should be last in chain while maintaining existing root handlers",
|
||||||
|
},
|
||||||
|
// Mixed domain tests
|
||||||
|
{
|
||||||
|
name: "Update with mixed domains - remove one of duplicate domain",
|
||||||
|
initialHandlers: baseMixedHandlers,
|
||||||
|
updates: []handlerWrapper{
|
||||||
|
{
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group1",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: "other.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-other",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedHandlers: map[string]string{
|
||||||
|
"upstream-group1": "example.com",
|
||||||
|
"upstream-other": "other.com",
|
||||||
|
},
|
||||||
|
description: "When updating mixed domains, should correctly handle removal of one duplicate while maintaining other domains",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Update with mixed domains - add new domain",
|
||||||
|
initialHandlers: baseMixedHandlers,
|
||||||
|
updates: []handlerWrapper{
|
||||||
|
{
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group1",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: "example.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-group2",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain - 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: "other.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-other",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
domain: "new.com",
|
||||||
|
handler: &mockHandler{
|
||||||
|
Id: "upstream-new",
|
||||||
|
},
|
||||||
|
priority: PriorityMatchDomain,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedHandlers: map[string]string{
|
||||||
|
"upstream-group1": "example.com",
|
||||||
|
"upstream-group2": "example.com",
|
||||||
|
"upstream-other": "other.com",
|
||||||
|
"upstream-new": "new.com",
|
||||||
|
},
|
||||||
|
description: "When updating mixed domains, should maintain existing duplicates and add new domain",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
server := &DefaultServer{
|
||||||
|
dnsMuxMap: tt.initialHandlers,
|
||||||
|
handlerChain: NewHandlerChain(),
|
||||||
|
service: &mockService{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the update
|
||||||
|
server.updateMux(tt.updates)
|
||||||
|
|
||||||
|
// Verify the results
|
||||||
|
assert.Equal(t, len(tt.expectedHandlers), len(server.dnsMuxMap),
|
||||||
|
"Number of handlers after update doesn't match expected")
|
||||||
|
|
||||||
|
// Check each expected handler
|
||||||
|
for id, expectedDomain := range tt.expectedHandlers {
|
||||||
|
handler, exists := server.dnsMuxMap[handlerID(id)]
|
||||||
|
assert.True(t, exists, "Expected handler %s not found", id)
|
||||||
|
if exists {
|
||||||
|
assert.Equal(t, expectedDomain, handler.domain,
|
||||||
|
"Domain mismatch for handler %s", id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no unexpected handlers exist
|
||||||
|
for handlerID := range server.dnsMuxMap {
|
||||||
|
_, expected := tt.expectedHandlers[string(handlerID)]
|
||||||
|
assert.True(t, expected, "Unexpected handler found: %s", handlerID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the handlerChain state and order
|
||||||
|
previousPriority := 0
|
||||||
|
for _, chainEntry := range server.handlerChain.handlers {
|
||||||
|
// Verify priority order
|
||||||
|
if previousPriority > 0 {
|
||||||
|
assert.True(t, chainEntry.Priority <= previousPriority,
|
||||||
|
"Handlers in chain not properly ordered by priority")
|
||||||
|
}
|
||||||
|
previousPriority = chainEntry.Priority
|
||||||
|
|
||||||
|
// Verify handler exists in mux
|
||||||
|
foundInMux := false
|
||||||
|
for _, muxEntry := range server.dnsMuxMap {
|
||||||
|
if chainEntry.Handler == muxEntry.handler &&
|
||||||
|
chainEntry.Priority == muxEntry.priority &&
|
||||||
|
chainEntry.Pattern == dns.Fqdn(muxEntry.domain) {
|
||||||
|
foundInMux = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, foundInMux,
|
||||||
|
"Handler in chain not found in dnsMuxMap")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ package dns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -40,6 +44,7 @@ type upstreamResolverBase struct {
|
|||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
upstreamClient upstreamClient
|
upstreamClient upstreamClient
|
||||||
upstreamServers []string
|
upstreamServers []string
|
||||||
|
domain string
|
||||||
disabled bool
|
disabled bool
|
||||||
failsCount atomic.Int32
|
failsCount atomic.Int32
|
||||||
successCount atomic.Int32
|
successCount atomic.Int32
|
||||||
@@ -53,12 +58,13 @@ type upstreamResolverBase struct {
|
|||||||
statusRecorder *peer.Status
|
statusRecorder *peer.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status) *upstreamResolverBase {
|
func newUpstreamResolverBase(ctx context.Context, statusRecorder *peer.Status, domain string) *upstreamResolverBase {
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
return &upstreamResolverBase{
|
return &upstreamResolverBase{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
cancel: cancel,
|
cancel: cancel,
|
||||||
|
domain: domain,
|
||||||
upstreamTimeout: upstreamTimeout,
|
upstreamTimeout: upstreamTimeout,
|
||||||
reactivatePeriod: reactivatePeriod,
|
reactivatePeriod: reactivatePeriod,
|
||||||
failsTillDeact: failsTillDeact,
|
failsTillDeact: failsTillDeact,
|
||||||
@@ -71,6 +77,17 @@ func (u *upstreamResolverBase) String() string {
|
|||||||
return fmt.Sprintf("upstream %v", u.upstreamServers)
|
return fmt.Sprintf("upstream %v", u.upstreamServers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ID returns the unique handler ID
|
||||||
|
func (u *upstreamResolverBase) id() handlerID {
|
||||||
|
servers := slices.Clone(u.upstreamServers)
|
||||||
|
slices.Sort(servers)
|
||||||
|
|
||||||
|
hash := sha256.New()
|
||||||
|
hash.Write([]byte(u.domain + ":"))
|
||||||
|
hash.Write([]byte(strings.Join(servers, ",")))
|
||||||
|
return handlerID("upstream-" + hex.EncodeToString(hash.Sum(nil)[:8]))
|
||||||
|
}
|
||||||
|
|
||||||
func (u *upstreamResolverBase) MatchSubdomains() bool {
|
func (u *upstreamResolverBase) MatchSubdomains() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -87,7 +104,7 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
u.checkUpstreamFails(err)
|
u.checkUpstreamFails(err)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
log.WithField("question", r.Question[0]).Trace("received an upstream question")
|
log.Tracef("received upstream question: domain=%s type=%v class=%v", r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass)
|
||||||
// set the AuthenticatedData flag and the EDNS0 buffer size to 4096 bytes to support larger dns records
|
// set the AuthenticatedData flag and the EDNS0 buffer size to 4096 bytes to support larger dns records
|
||||||
if r.Extra == nil {
|
if r.Extra == nil {
|
||||||
r.SetEdns0(4096, false)
|
r.SetEdns0(4096, false)
|
||||||
@@ -96,6 +113,7 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case <-u.ctx.Done():
|
case <-u.ctx.Done():
|
||||||
|
log.Tracef("%s has been stopped", u)
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
@@ -112,41 +130,36 @@ func (u *upstreamResolverBase) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, context.DeadlineExceeded) || isTimeout(err) {
|
if errors.Is(err, context.DeadlineExceeded) || isTimeout(err) {
|
||||||
log.WithError(err).WithField("upstream", upstream).
|
log.Warnf("upstream %s timed out for question domain=%s", upstream, r.Question[0].Name)
|
||||||
Warn("got an error while connecting to upstream")
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
u.failsCount.Add(1)
|
log.Warnf("failed to query upstream %s for question domain=%s: %s", upstream, r.Question[0].Name, err)
|
||||||
log.WithError(err).WithField("upstream", upstream).
|
continue
|
||||||
Error("got other error while querying the upstream")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if rm == nil {
|
if rm == nil || !rm.Response {
|
||||||
log.WithError(err).WithField("upstream", upstream).
|
log.Warnf("no response from upstream %s for question domain=%s", upstream, r.Question[0].Name)
|
||||||
Warn("no response from upstream")
|
continue
|
||||||
return
|
|
||||||
}
|
|
||||||
// those checks need to be independent of each other due to memory address issues
|
|
||||||
if !rm.Response {
|
|
||||||
log.WithError(err).WithField("upstream", upstream).
|
|
||||||
Warn("no response from upstream")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u.successCount.Add(1)
|
u.successCount.Add(1)
|
||||||
log.Tracef("took %s to query the upstream %s", t, upstream)
|
log.Tracef("took %s to query the upstream %s for question domain=%s", t, upstream, r.Question[0].Name)
|
||||||
|
|
||||||
err = w.WriteMsg(rm)
|
if err = w.WriteMsg(rm); err != nil {
|
||||||
if err != nil {
|
log.Errorf("failed to write DNS response for question domain=%s: %s", r.Question[0].Name, err)
|
||||||
log.WithError(err).Error("got an error while writing the upstream resolver response")
|
|
||||||
}
|
}
|
||||||
// count the fails only if they happen sequentially
|
// count the fails only if they happen sequentially
|
||||||
u.failsCount.Store(0)
|
u.failsCount.Store(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
u.failsCount.Add(1)
|
u.failsCount.Add(1)
|
||||||
log.Error("all queries to the upstream nameservers failed with timeout")
|
log.Errorf("all queries to the %s failed for question domain=%s", u, r.Question[0].Name)
|
||||||
|
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetRcode(r, dns.RcodeServerFailure)
|
||||||
|
if err := w.WriteMsg(m); err != nil {
|
||||||
|
log.Errorf("failed to write error response for %s for question domain=%s: %s", u, r.Question[0].Name, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkUpstreamFails counts fails and disables or enables upstream resolving
|
// checkUpstreamFails counts fails and disables or enables upstream resolving
|
||||||
|
|||||||
@@ -27,8 +27,9 @@ func newUpstreamResolver(
|
|||||||
_ *net.IPNet,
|
_ *net.IPNet,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
hostsDNSHolder *hostsDNSHolder,
|
hostsDNSHolder *hostsDNSHolder,
|
||||||
|
domain string,
|
||||||
) (*upstreamResolver, error) {
|
) (*upstreamResolver, error) {
|
||||||
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder)
|
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, domain)
|
||||||
c := &upstreamResolver{
|
c := &upstreamResolver{
|
||||||
upstreamResolverBase: upstreamResolverBase,
|
upstreamResolverBase: upstreamResolverBase,
|
||||||
hostsDNSHolder: hostsDNSHolder,
|
hostsDNSHolder: hostsDNSHolder,
|
||||||
|
|||||||
@@ -23,8 +23,9 @@ func newUpstreamResolver(
|
|||||||
_ *net.IPNet,
|
_ *net.IPNet,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
_ *hostsDNSHolder,
|
_ *hostsDNSHolder,
|
||||||
|
domain string,
|
||||||
) (*upstreamResolver, error) {
|
) (*upstreamResolver, error) {
|
||||||
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder)
|
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, domain)
|
||||||
nonIOS := &upstreamResolver{
|
nonIOS := &upstreamResolver{
|
||||||
upstreamResolverBase: upstreamResolverBase,
|
upstreamResolverBase: upstreamResolverBase,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ func newUpstreamResolver(
|
|||||||
net *net.IPNet,
|
net *net.IPNet,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
_ *hostsDNSHolder,
|
_ *hostsDNSHolder,
|
||||||
|
domain string,
|
||||||
) (*upstreamResolverIOS, error) {
|
) (*upstreamResolverIOS, error) {
|
||||||
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder)
|
upstreamResolverBase := newUpstreamResolverBase(ctx, statusRecorder, domain)
|
||||||
|
|
||||||
ios := &upstreamResolverIOS{
|
ios := &upstreamResolverIOS{
|
||||||
upstreamResolverBase: upstreamResolverBase,
|
upstreamResolverBase: upstreamResolverBase,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) {
|
|||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
cancelCTX bool
|
cancelCTX bool
|
||||||
expectedAnswer string
|
expectedAnswer string
|
||||||
|
acceptNXDomain bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Should Resolve A Record",
|
name: "Should Resolve A Record",
|
||||||
@@ -40,7 +41,7 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) {
|
|||||||
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
|
inputMSG: new(dns.Msg).SetQuestion("one.one.one.one.", dns.TypeA),
|
||||||
InputServers: []string{"8.0.0.0:53", "8.0.0.1:53"},
|
InputServers: []string{"8.0.0.0:53", "8.0.0.1:53"},
|
||||||
timeout: 200 * time.Millisecond,
|
timeout: 200 * time.Millisecond,
|
||||||
responseShouldBeNil: true,
|
acceptNXDomain: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Should Not Resolve If Parent Context Is Canceled",
|
name: "Should Not Resolve If Parent Context Is Canceled",
|
||||||
@@ -51,14 +52,11 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) {
|
|||||||
responseShouldBeNil: true,
|
responseShouldBeNil: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// should resolve if first upstream times out
|
|
||||||
// should not write when both fails
|
|
||||||
// should not resolve if parent context is canceled
|
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
t.Run(testCase.name, func(t *testing.T) {
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
ctx, cancel := context.WithCancel(context.TODO())
|
ctx, cancel := context.WithCancel(context.TODO())
|
||||||
resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}, nil, nil)
|
resolver, _ := newUpstreamResolver(ctx, "", net.IP{}, &net.IPNet{}, nil, nil, ".")
|
||||||
resolver.upstreamServers = testCase.InputServers
|
resolver.upstreamServers = testCase.InputServers
|
||||||
resolver.upstreamTimeout = testCase.timeout
|
resolver.upstreamTimeout = testCase.timeout
|
||||||
if testCase.cancelCTX {
|
if testCase.cancelCTX {
|
||||||
@@ -84,6 +82,11 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) {
|
|||||||
t.Fatalf("should write a response message")
|
t.Fatalf("should write a response message")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if testCase.acceptNXDomain && responseMSG.Rcode == dns.RcodeNameError {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if testCase.expectedAnswer != "" {
|
||||||
foundAnswer := false
|
foundAnswer := false
|
||||||
for _, answer := range responseMSG.Answer {
|
for _, answer := range responseMSG.Answer {
|
||||||
if strings.Contains(answer.String(), testCase.expectedAnswer) {
|
if strings.Contains(answer.String(), testCase.expectedAnswer) {
|
||||||
@@ -95,6 +98,7 @@ func TestUpstreamResolver_ServeDNS(t *testing.T) {
|
|||||||
if !foundAnswer {
|
if !foundAnswer {
|
||||||
t.Errorf("couldn't find the required answer, %s, in the dns response", testCase.expectedAnswer)
|
t.Errorf("couldn't find the required answer, %s, in the dns response", testCase.expectedAnswer)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func (m *Manager) Stop(ctx context.Context) error {
|
|||||||
func (h *Manager) allowDNSFirewall() error {
|
func (h *Manager) allowDNSFirewall() error {
|
||||||
dport := &firewall.Port{
|
dport := &firewall.Port{
|
||||||
IsRange: false,
|
IsRange: false,
|
||||||
Values: []int{ListenPort},
|
Values: []uint16{ListenPort},
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.firewall == nil {
|
if h.firewall == nil {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/go-multierror"
|
"github.com/hashicorp/go-multierror"
|
||||||
@@ -43,13 +42,13 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/routemanager"
|
"github.com/netbirdio/netbird/client/internal/routemanager"
|
||||||
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||||
"github.com/netbirdio/netbird/client/internal/statemanager"
|
"github.com/netbirdio/netbird/client/internal/statemanager"
|
||||||
|
"github.com/netbirdio/netbird/management/domain"
|
||||||
semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group"
|
semaphoregroup "github.com/netbirdio/netbird/util/semaphore-group"
|
||||||
|
|
||||||
nbssh "github.com/netbirdio/netbird/client/ssh"
|
nbssh "github.com/netbirdio/netbird/client/ssh"
|
||||||
"github.com/netbirdio/netbird/client/system"
|
"github.com/netbirdio/netbird/client/system"
|
||||||
nbdns "github.com/netbirdio/netbird/dns"
|
nbdns "github.com/netbirdio/netbird/dns"
|
||||||
mgm "github.com/netbirdio/netbird/management/client"
|
mgm "github.com/netbirdio/netbird/management/client"
|
||||||
"github.com/netbirdio/netbird/management/domain"
|
|
||||||
mgmProto "github.com/netbirdio/netbird/management/proto"
|
mgmProto "github.com/netbirdio/netbird/management/proto"
|
||||||
auth "github.com/netbirdio/netbird/relay/auth/hmac"
|
auth "github.com/netbirdio/netbird/relay/auth/hmac"
|
||||||
relayClient "github.com/netbirdio/netbird/relay/client"
|
relayClient "github.com/netbirdio/netbird/relay/client"
|
||||||
@@ -146,7 +145,7 @@ type Engine struct {
|
|||||||
STUNs []*stun.URI
|
STUNs []*stun.URI
|
||||||
// TURNs is a list of STUN servers used by ICE
|
// TURNs is a list of STUN servers used by ICE
|
||||||
TURNs []*stun.URI
|
TURNs []*stun.URI
|
||||||
stunTurn atomic.Value
|
stunTurn icemaker.StunTurn
|
||||||
|
|
||||||
clientCtx context.Context
|
clientCtx context.Context
|
||||||
clientCancel context.CancelFunc
|
clientCancel context.CancelFunc
|
||||||
@@ -175,8 +174,6 @@ type Engine struct {
|
|||||||
|
|
||||||
dnsServer dns.Server
|
dnsServer dns.Server
|
||||||
|
|
||||||
probes *ProbeHolder
|
|
||||||
|
|
||||||
// checks are the client-applied posture checks that need to be evaluated on the client
|
// checks are the client-applied posture checks that need to be evaluated on the client
|
||||||
checks []*mgmProto.Checks
|
checks []*mgmProto.Checks
|
||||||
|
|
||||||
@@ -196,7 +193,11 @@ type Peer struct {
|
|||||||
WgAllowedIps string
|
WgAllowedIps string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEngine creates a new Connection Engine
|
type localIpUpdater interface {
|
||||||
|
UpdateLocalIPs() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEngine creates a new Connection Engine with probes attached
|
||||||
func NewEngine(
|
func NewEngine(
|
||||||
clientCtx context.Context,
|
clientCtx context.Context,
|
||||||
clientCancel context.CancelFunc,
|
clientCancel context.CancelFunc,
|
||||||
@@ -207,33 +208,6 @@ func NewEngine(
|
|||||||
mobileDep MobileDependency,
|
mobileDep MobileDependency,
|
||||||
statusRecorder *peer.Status,
|
statusRecorder *peer.Status,
|
||||||
checks []*mgmProto.Checks,
|
checks []*mgmProto.Checks,
|
||||||
) *Engine {
|
|
||||||
return NewEngineWithProbes(
|
|
||||||
clientCtx,
|
|
||||||
clientCancel,
|
|
||||||
signalClient,
|
|
||||||
mgmClient,
|
|
||||||
relayManager,
|
|
||||||
config,
|
|
||||||
mobileDep,
|
|
||||||
statusRecorder,
|
|
||||||
nil,
|
|
||||||
checks,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEngineWithProbes creates a new Connection Engine with probes attached
|
|
||||||
func NewEngineWithProbes(
|
|
||||||
clientCtx context.Context,
|
|
||||||
clientCancel context.CancelFunc,
|
|
||||||
signalClient signal.Client,
|
|
||||||
mgmClient mgm.Client,
|
|
||||||
relayManager *relayClient.Manager,
|
|
||||||
config *EngineConfig,
|
|
||||||
mobileDep MobileDependency,
|
|
||||||
statusRecorder *peer.Status,
|
|
||||||
probes *ProbeHolder,
|
|
||||||
checks []*mgmProto.Checks,
|
|
||||||
) *Engine {
|
) *Engine {
|
||||||
engine := &Engine{
|
engine := &Engine{
|
||||||
clientCtx: clientCtx,
|
clientCtx: clientCtx,
|
||||||
@@ -251,7 +225,6 @@ func NewEngineWithProbes(
|
|||||||
networkSerial: 0,
|
networkSerial: 0,
|
||||||
sshServerFunc: nbssh.DefaultSSHServer,
|
sshServerFunc: nbssh.DefaultSSHServer,
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
probes: probes,
|
|
||||||
checks: checks,
|
checks: checks,
|
||||||
connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit),
|
connSemaphore: semaphoregroup.NewSemaphoreGroup(connInitLimit),
|
||||||
}
|
}
|
||||||
@@ -450,7 +423,6 @@ func (e *Engine) Start() error {
|
|||||||
|
|
||||||
e.receiveSignalEvents()
|
e.receiveSignalEvents()
|
||||||
e.receiveManagementEvents()
|
e.receiveManagementEvents()
|
||||||
e.receiveProbeEvents()
|
|
||||||
|
|
||||||
// starting network monitor at the very last to avoid disruptions
|
// starting network monitor at the very last to avoid disruptions
|
||||||
e.startNetworkMonitor()
|
e.startNetworkMonitor()
|
||||||
@@ -465,7 +437,7 @@ func (e *Engine) createFirewall() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
e.firewall, err = firewall.NewFirewall(e.wgInterface, e.stateManager)
|
e.firewall, err = firewall.NewFirewall(e.wgInterface, e.stateManager, e.config.DisableServerRoutes)
|
||||||
if err != nil || e.firewall == nil {
|
if err != nil || e.firewall == nil {
|
||||||
log.Errorf("failed creating firewall manager: %s", err)
|
log.Errorf("failed creating firewall manager: %s", err)
|
||||||
return nil
|
return nil
|
||||||
@@ -495,7 +467,7 @@ func (e *Engine) initFirewall() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rosenpassPort := e.rpManager.GetAddress().Port
|
rosenpassPort := e.rpManager.GetAddress().Port
|
||||||
port := manager.Port{Values: []int{rosenpassPort}}
|
port := manager.Port{Values: []uint16{uint16(rosenpassPort)}}
|
||||||
|
|
||||||
// this rule is static and will be torn down on engine down by the firewall manager
|
// this rule is static and will be torn down on engine down by the firewall manager
|
||||||
if _, err := e.firewall.AddPeerFiltering(
|
if _, err := e.firewall.AddPeerFiltering(
|
||||||
@@ -639,8 +611,8 @@ func (e *Engine) handleSync(update *mgmProto.SyncResponse) error {
|
|||||||
e.syncMsgMux.Lock()
|
e.syncMsgMux.Lock()
|
||||||
defer e.syncMsgMux.Unlock()
|
defer e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
if update.GetWiretrusteeConfig() != nil {
|
if update.GetNetbirdConfig() != nil {
|
||||||
wCfg := update.GetWiretrusteeConfig()
|
wCfg := update.GetNetbirdConfig()
|
||||||
err := e.updateTURNs(wCfg.GetTurns())
|
err := e.updateTURNs(wCfg.GetTurns())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("update TURNs: %w", err)
|
return fmt.Errorf("update TURNs: %w", err)
|
||||||
@@ -915,6 +887,14 @@ func (e *Engine) updateNetworkMap(networkMap *mgmProto.NetworkMap) error {
|
|||||||
e.acl.ApplyFiltering(networkMap)
|
e.acl.ApplyFiltering(networkMap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if e.firewall != nil {
|
||||||
|
if localipfw, ok := e.firewall.(localIpUpdater); ok {
|
||||||
|
if err := localipfw.UpdateLocalIPs(); err != nil {
|
||||||
|
log.Errorf("failed to update local IPs: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DNS forwarder
|
// DNS forwarder
|
||||||
dnsRouteFeatureFlag := toDNSFeatureFlag(networkMap)
|
dnsRouteFeatureFlag := toDNSFeatureFlag(networkMap)
|
||||||
dnsRouteDomains := toRouteDomains(e.config.WgPrivateKey.PublicKey().String(), networkMap.GetRoutes())
|
dnsRouteDomains := toRouteDomains(e.config.WgPrivateKey.PublicKey().String(), networkMap.GetRoutes())
|
||||||
@@ -1478,6 +1458,11 @@ func (e *Engine) GetRouteManager() routemanager.Manager {
|
|||||||
return e.routeManager
|
return e.routeManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFirewallManager returns the firewall manager
|
||||||
|
func (e *Engine) GetFirewallManager() manager.Manager {
|
||||||
|
return e.firewall
|
||||||
|
}
|
||||||
|
|
||||||
func findIPFromInterfaceName(ifaceName string) (net.IP, error) {
|
func findIPFromInterfaceName(ifaceName string) (net.IP, error) {
|
||||||
iface, err := net.InterfaceByName(ifaceName)
|
iface, err := net.InterfaceByName(ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1513,54 +1498,32 @@ func (e *Engine) getRosenpassAddr() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) receiveProbeEvents() {
|
// RunHealthProbes executes health checks for Signal, Management, Relay and WireGuard services
|
||||||
if e.probes == nil {
|
// and updates the status recorder with the latest states.
|
||||||
return
|
func (e *Engine) RunHealthProbes() bool {
|
||||||
}
|
signalHealthy := e.signal.IsHealthy()
|
||||||
if e.probes.SignalProbe != nil {
|
log.Debugf("signal health check: healthy=%t", signalHealthy)
|
||||||
go e.probes.SignalProbe.Receive(e.ctx, func() bool {
|
|
||||||
healthy := e.signal.IsHealthy()
|
|
||||||
log.Debugf("received signal probe request, healthy: %t", healthy)
|
|
||||||
return healthy
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.probes.MgmProbe != nil {
|
managementHealthy := e.mgmClient.IsHealthy()
|
||||||
go e.probes.MgmProbe.Receive(e.ctx, func() bool {
|
log.Debugf("management health check: healthy=%t", managementHealthy)
|
||||||
healthy := e.mgmClient.IsHealthy()
|
|
||||||
log.Debugf("received management probe request, healthy: %t", healthy)
|
|
||||||
return healthy
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.probes.RelayProbe != nil {
|
|
||||||
go e.probes.RelayProbe.Receive(e.ctx, func() bool {
|
|
||||||
healthy := true
|
|
||||||
|
|
||||||
results := append(e.probeSTUNs(), e.probeTURNs()...)
|
results := append(e.probeSTUNs(), e.probeTURNs()...)
|
||||||
e.statusRecorder.UpdateRelayStates(results)
|
e.statusRecorder.UpdateRelayStates(results)
|
||||||
|
|
||||||
// A single failed server will result in a "failed" probe
|
relayHealthy := true
|
||||||
for _, res := range results {
|
for _, res := range results {
|
||||||
if res.Err != nil {
|
if res.Err != nil {
|
||||||
healthy = false
|
relayHealthy = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log.Debugf("relay health check: healthy=%t", relayHealthy)
|
||||||
log.Debugf("received relay probe request, healthy: %t", healthy)
|
|
||||||
return healthy
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.probes.WgProbe != nil {
|
|
||||||
go e.probes.WgProbe.Receive(e.ctx, func() bool {
|
|
||||||
log.Debug("received wg probe request")
|
|
||||||
|
|
||||||
for _, key := range e.peerStore.PeersPubKey() {
|
for _, key := range e.peerStore.PeersPubKey() {
|
||||||
wgStats, err := e.wgInterface.GetStats(key)
|
wgStats, err := e.wgInterface.GetStats(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("failed to get wg stats for peer %s: %s", key, err)
|
log.Debugf("failed to get wg stats for peer %s: %s", key, err)
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
// wgStats could be zero value, in which case we just reset the stats
|
// wgStats could be zero value, in which case we just reset the stats
|
||||||
if err := e.statusRecorder.UpdateWireGuardPeerState(key, wgStats); err != nil {
|
if err := e.statusRecorder.UpdateWireGuardPeerState(key, wgStats); err != nil {
|
||||||
@@ -1568,17 +1531,25 @@ func (e *Engine) receiveProbeEvents() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
allHealthy := signalHealthy && managementHealthy && relayHealthy
|
||||||
})
|
log.Debugf("all health checks completed: healthy=%t", allHealthy)
|
||||||
}
|
return allHealthy
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) probeSTUNs() []relay.ProbeResult {
|
func (e *Engine) probeSTUNs() []relay.ProbeResult {
|
||||||
return relay.ProbeAll(e.ctx, relay.ProbeSTUN, e.STUNs)
|
e.syncMsgMux.Lock()
|
||||||
|
stuns := slices.Clone(e.STUNs)
|
||||||
|
e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
return relay.ProbeAll(e.ctx, relay.ProbeSTUN, stuns)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) probeTURNs() []relay.ProbeResult {
|
func (e *Engine) probeTURNs() []relay.ProbeResult {
|
||||||
return relay.ProbeAll(e.ctx, relay.ProbeTURN, e.TURNs)
|
e.syncMsgMux.Lock()
|
||||||
|
turns := slices.Clone(e.TURNs)
|
||||||
|
e.syncMsgMux.Unlock()
|
||||||
|
|
||||||
|
return relay.ProbeAll(e.ctx, relay.ProbeTURN, turns)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) restartEngine() {
|
func (e *Engine) restartEngine() {
|
||||||
@@ -1703,6 +1674,14 @@ func (e *Engine) GetLatestNetworkMap() (*mgmProto.NetworkMap, error) {
|
|||||||
return nm, nil
|
return nm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetWgAddr returns the wireguard address
|
||||||
|
func (e *Engine) GetWgAddr() net.IP {
|
||||||
|
if e.wgInterface == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.wgInterface.Address().IP
|
||||||
|
}
|
||||||
|
|
||||||
// updateDNSForwarder start or stop the DNS forwarder based on the domains and the feature flag
|
// updateDNSForwarder start or stop the DNS forwarder based on the domains and the feature flag
|
||||||
func (e *Engine) updateDNSForwarder(enabled bool, domains []string) {
|
func (e *Engine) updateDNSForwarder(enabled bool, domains []string) {
|
||||||
if !enabled {
|
if !enabled {
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ const (
|
|||||||
defaultWgKeepAlive = 25 * time.Second
|
defaultWgKeepAlive = 25 * time.Second
|
||||||
|
|
||||||
connPriorityRelay ConnPriority = 1
|
connPriorityRelay ConnPriority = 1
|
||||||
connPriorityICETurn ConnPriority = 1
|
connPriorityICETurn ConnPriority = 2
|
||||||
connPriorityICEP2P ConnPriority = 2
|
connPriorityICEP2P ConnPriority = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
type WgConfig struct {
|
type WgConfig struct {
|
||||||
@@ -66,14 +66,6 @@ type ConnConfig struct {
|
|||||||
ICEConfig icemaker.Config
|
ICEConfig icemaker.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkerCallbacks struct {
|
|
||||||
OnRelayReadyCallback func(info RelayConnInfo)
|
|
||||||
OnRelayStatusChanged func(ConnStatus)
|
|
||||||
|
|
||||||
OnICEConnReadyCallback func(ConnPriority, ICEConnInfo)
|
|
||||||
OnICEStatusChanged func(ConnStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -135,21 +127,11 @@ func NewConn(engineCtx context.Context, config ConnConfig, statusRecorder *Statu
|
|||||||
semaphore: semaphore,
|
semaphore: semaphore,
|
||||||
}
|
}
|
||||||
|
|
||||||
rFns := WorkerRelayCallbacks{
|
|
||||||
OnConnReady: conn.relayConnectionIsReady,
|
|
||||||
OnDisconnected: conn.onWorkerRelayStateDisconnected,
|
|
||||||
}
|
|
||||||
|
|
||||||
wFns := WorkerICECallbacks{
|
|
||||||
OnConnReady: conn.iCEConnectionIsReady,
|
|
||||||
OnStatusChanged: conn.onWorkerICEStateDisconnected,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrl := isController(config)
|
ctrl := isController(config)
|
||||||
conn.workerRelay = NewWorkerRelay(connLog, ctrl, config, relayManager, rFns)
|
conn.workerRelay = NewWorkerRelay(connLog, ctrl, config, conn, relayManager)
|
||||||
|
|
||||||
relayIsSupportedLocally := conn.workerRelay.RelayIsSupportedLocally()
|
relayIsSupportedLocally := conn.workerRelay.RelayIsSupportedLocally()
|
||||||
conn.workerICE, err = NewWorkerICE(ctx, connLog, config, signaler, iFaceDiscover, statusRecorder, relayIsSupportedLocally, wFns)
|
conn.workerICE, err = NewWorkerICE(ctx, connLog, config, conn, signaler, iFaceDiscover, statusRecorder, relayIsSupportedLocally)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -304,7 +286,7 @@ func (conn *Conn) GetKey() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// configureConnection starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
|
// configureConnection starts proxying traffic from/to local Wireguard and sets connection status to StatusConnected
|
||||||
func (conn *Conn) iCEConnectionIsReady(priority ConnPriority, iceConnInfo ICEConnInfo) {
|
func (conn *Conn) onICEConnectionIsReady(priority ConnPriority, iceConnInfo ICEConnInfo) {
|
||||||
conn.mu.Lock()
|
conn.mu.Lock()
|
||||||
defer conn.mu.Unlock()
|
defer conn.mu.Unlock()
|
||||||
|
|
||||||
@@ -376,7 +358,7 @@ func (conn *Conn) iCEConnectionIsReady(priority ConnPriority, iceConnInfo ICECon
|
|||||||
}
|
}
|
||||||
|
|
||||||
// todo review to make sense to handle connecting and disconnected status also?
|
// todo review to make sense to handle connecting and disconnected status also?
|
||||||
func (conn *Conn) onWorkerICEStateDisconnected(newState ConnStatus) {
|
func (conn *Conn) onICEStateDisconnected() {
|
||||||
conn.mu.Lock()
|
conn.mu.Lock()
|
||||||
defer conn.mu.Unlock()
|
defer conn.mu.Unlock()
|
||||||
|
|
||||||
@@ -384,7 +366,7 @@ func (conn *Conn) onWorkerICEStateDisconnected(newState ConnStatus) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conn.log.Tracef("ICE connection state changed to %s", newState)
|
conn.log.Tracef("ICE connection state changed to disconnected")
|
||||||
|
|
||||||
if conn.wgProxyICE != nil {
|
if conn.wgProxyICE != nil {
|
||||||
if err := conn.wgProxyICE.CloseConn(); err != nil {
|
if err := conn.wgProxyICE.CloseConn(); err != nil {
|
||||||
@@ -404,10 +386,11 @@ func (conn *Conn) onWorkerICEStateDisconnected(newState ConnStatus) {
|
|||||||
conn.currentConnPriority = connPriorityRelay
|
conn.currentConnPriority = connPriorityRelay
|
||||||
}
|
}
|
||||||
|
|
||||||
changed := conn.statusICE.Get() != newState && newState != StatusConnecting
|
changed := conn.statusICE.Get() != StatusDisconnected
|
||||||
conn.statusICE.Set(newState)
|
if changed {
|
||||||
|
conn.guard.SetICEConnDisconnected()
|
||||||
conn.guard.SetICEConnDisconnected(changed)
|
}
|
||||||
|
conn.statusICE.Set(StatusDisconnected)
|
||||||
|
|
||||||
peerState := State{
|
peerState := State{
|
||||||
PubKey: conn.config.Key,
|
PubKey: conn.config.Key,
|
||||||
@@ -422,7 +405,7 @@ func (conn *Conn) onWorkerICEStateDisconnected(newState ConnStatus) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *Conn) relayConnectionIsReady(rci RelayConnInfo) {
|
func (conn *Conn) onRelayConnectionIsReady(rci RelayConnInfo) {
|
||||||
conn.mu.Lock()
|
conn.mu.Lock()
|
||||||
defer conn.mu.Unlock()
|
defer conn.mu.Unlock()
|
||||||
|
|
||||||
@@ -474,7 +457,7 @@ func (conn *Conn) relayConnectionIsReady(rci RelayConnInfo) {
|
|||||||
conn.doOnConnected(rci.rosenpassPubKey, rci.rosenpassAddr)
|
conn.doOnConnected(rci.rosenpassPubKey, rci.rosenpassAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *Conn) onWorkerRelayStateDisconnected() {
|
func (conn *Conn) onRelayDisconnected() {
|
||||||
conn.mu.Lock()
|
conn.mu.Lock()
|
||||||
defer conn.mu.Unlock()
|
defer conn.mu.Unlock()
|
||||||
|
|
||||||
@@ -497,8 +480,10 @@ func (conn *Conn) onWorkerRelayStateDisconnected() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
changed := conn.statusRelay.Get() != StatusDisconnected
|
changed := conn.statusRelay.Get() != StatusDisconnected
|
||||||
|
if changed {
|
||||||
|
conn.guard.SetRelayedConnDisconnected()
|
||||||
|
}
|
||||||
conn.statusRelay.Set(StatusDisconnected)
|
conn.statusRelay.Set(StatusDisconnected)
|
||||||
conn.guard.SetRelayedConnDisconnected(changed)
|
|
||||||
|
|
||||||
peerState := State{
|
peerState := State{
|
||||||
PubKey: conn.config.Key,
|
PubKey: conn.config.Key,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/magiconair/properties/assert"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConnStatus_String(t *testing.T) {
|
func TestConnStatus_String(t *testing.T) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/magiconair/properties/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/iface"
|
"github.com/netbirdio/netbird/client/iface"
|
||||||
"github.com/netbirdio/netbird/client/internal/peer/guard"
|
"github.com/netbirdio/netbird/client/internal/peer/guard"
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ type Guard struct {
|
|||||||
isConnectedOnAllWay isConnectedFunc
|
isConnectedOnAllWay isConnectedFunc
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
srWatcher *SRWatcher
|
srWatcher *SRWatcher
|
||||||
relayedConnDisconnected chan bool
|
relayedConnDisconnected chan struct{}
|
||||||
iCEConnDisconnected chan bool
|
iCEConnDisconnected chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGuard(log *log.Entry, isController bool, isConnectedFn isConnectedFunc, timeout time.Duration, srWatcher *SRWatcher) *Guard {
|
func NewGuard(log *log.Entry, isController bool, isConnectedFn isConnectedFunc, timeout time.Duration, srWatcher *SRWatcher) *Guard {
|
||||||
@@ -41,8 +41,8 @@ func NewGuard(log *log.Entry, isController bool, isConnectedFn isConnectedFunc,
|
|||||||
isConnectedOnAllWay: isConnectedFn,
|
isConnectedOnAllWay: isConnectedFn,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
srWatcher: srWatcher,
|
srWatcher: srWatcher,
|
||||||
relayedConnDisconnected: make(chan bool, 1),
|
relayedConnDisconnected: make(chan struct{}, 1),
|
||||||
iCEConnDisconnected: make(chan bool, 1),
|
iCEConnDisconnected: make(chan struct{}, 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,16 +54,16 @@ func (g *Guard) Start(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Guard) SetRelayedConnDisconnected(changed bool) {
|
func (g *Guard) SetRelayedConnDisconnected() {
|
||||||
select {
|
select {
|
||||||
case g.relayedConnDisconnected <- changed:
|
case g.relayedConnDisconnected <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Guard) SetICEConnDisconnected(changed bool) {
|
func (g *Guard) SetICEConnDisconnected() {
|
||||||
select {
|
select {
|
||||||
case g.iCEConnDisconnected <- changed:
|
case g.iCEConnDisconnected <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,19 +96,13 @@ func (g *Guard) reconnectLoopWithRetry(ctx context.Context) {
|
|||||||
g.triggerOfferSending()
|
g.triggerOfferSending()
|
||||||
}
|
}
|
||||||
|
|
||||||
case changed := <-g.relayedConnDisconnected:
|
case <-g.relayedConnDisconnected:
|
||||||
if !changed {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
g.log.Debugf("Relay connection changed, reset reconnection ticker")
|
g.log.Debugf("Relay connection changed, reset reconnection ticker")
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
ticker = g.prepareExponentTicker(ctx)
|
ticker = g.prepareExponentTicker(ctx)
|
||||||
tickerChannel = ticker.C
|
tickerChannel = ticker.C
|
||||||
|
|
||||||
case changed := <-g.iCEConnDisconnected:
|
case <-g.iCEConnDisconnected:
|
||||||
if !changed {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
g.log.Debugf("ICE connection changed, reset reconnection ticker")
|
g.log.Debugf("ICE connection changed, reset reconnection ticker")
|
||||||
ticker.Stop()
|
ticker.Stop()
|
||||||
ticker = g.prepareExponentTicker(ctx)
|
ticker = g.prepareExponentTicker(ctx)
|
||||||
@@ -138,16 +132,10 @@ func (g *Guard) listenForDisconnectEvents(ctx context.Context) {
|
|||||||
g.log.Infof("start listen for reconnect events...")
|
g.log.Infof("start listen for reconnect events...")
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case changed := <-g.relayedConnDisconnected:
|
case <-g.relayedConnDisconnected:
|
||||||
if !changed {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
g.log.Debugf("Relay connection changed, triggering reconnect")
|
g.log.Debugf("Relay connection changed, triggering reconnect")
|
||||||
g.triggerOfferSending()
|
g.triggerOfferSending()
|
||||||
case changed := <-g.iCEConnDisconnected:
|
case <-g.iCEConnDisconnected:
|
||||||
if !changed {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
g.log.Debugf("ICE state changed, try to send new offer")
|
g.log.Debugf("ICE state changed, try to send new offer")
|
||||||
g.triggerOfferSending()
|
g.triggerOfferSending()
|
||||||
case <-srReconnectedChan:
|
case <-srReconnectedChan:
|
||||||
|
|||||||
22
client/internal/peer/ice/StunTurn.go
Normal file
22
client/internal/peer/ice/StunTurn.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package ice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/pion/stun/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StunTurn atomic.Value
|
||||||
|
|
||||||
|
func (s *StunTurn) Load() []*stun.URI {
|
||||||
|
v := (*atomic.Value)(s).Load()
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.([]*stun.URI)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StunTurn) Store(value []*stun.URI) {
|
||||||
|
(*atomic.Value)(s).Store(value)
|
||||||
|
}
|
||||||
13
client/internal/peer/ice/StunTurn_test.go
Normal file
13
client/internal/peer/ice/StunTurn_test.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package ice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStunTurn_LoadEmpty(t *testing.T) {
|
||||||
|
var stStunTurn StunTurn
|
||||||
|
got := stStunTurn.Load()
|
||||||
|
if len(got) != 0 {
|
||||||
|
t.Errorf("StunTurn.Load() = %v, want %v", got, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
|
|
||||||
"github.com/pion/ice/v3"
|
"github.com/pion/ice/v3"
|
||||||
"github.com/pion/randutil"
|
"github.com/pion/randutil"
|
||||||
"github.com/pion/stun/v2"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/netbirdio/netbird/client/internal/stdnet"
|
"github.com/netbirdio/netbird/client/internal/stdnet"
|
||||||
@@ -39,7 +38,7 @@ func NewAgent(iFaceDiscover stdnet.ExternalIFaceDiscover, config Config, candida
|
|||||||
agentConfig := &ice.AgentConfig{
|
agentConfig := &ice.AgentConfig{
|
||||||
MulticastDNSMode: ice.MulticastDNSModeDisabled,
|
MulticastDNSMode: ice.MulticastDNSModeDisabled,
|
||||||
NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6},
|
NetworkTypes: []ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6},
|
||||||
Urls: config.StunTurn.Load().([]*stun.URI),
|
Urls: config.StunTurn.Load(),
|
||||||
CandidateTypes: candidateTypes,
|
CandidateTypes: candidateTypes,
|
||||||
InterfaceFilter: stdnet.InterfaceFilter(config.InterfaceBlackList),
|
InterfaceFilter: stdnet.InterfaceFilter(config.InterfaceBlackList),
|
||||||
UDPMux: config.UDPMux,
|
UDPMux: config.UDPMux,
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
package ice
|
package ice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"github.com/pion/ice/v3"
|
"github.com/pion/ice/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// StunTurn is a list of STUN and TURN URLs
|
// StunTurn is a list of STUN and TURN URLs
|
||||||
StunTurn *atomic.Value // []*stun.URI
|
StunTurn *StunTurn // []*stun.URI
|
||||||
|
|
||||||
// InterfaceBlackList is a list of machine interfaces that should be filtered out by ICE Candidate gathering
|
// InterfaceBlackList is a list of machine interfaces that should be filtered out by ICE Candidate gathering
|
||||||
// (e.g. if eth0 is in the list, host candidate of this interface won't be used)
|
// (e.g. if eth0 is in the list, host candidate of this interface won't be used)
|
||||||
|
|||||||
@@ -721,7 +721,9 @@ func (d *Status) GetRelayStates() []relay.ProbeResult {
|
|||||||
func (d *Status) GetDNSStates() []NSGroupState {
|
func (d *Status) GetDNSStates() []NSGroupState {
|
||||||
d.mux.Lock()
|
d.mux.Lock()
|
||||||
defer d.mux.Unlock()
|
defer d.mux.Unlock()
|
||||||
return d.nsGroupStates
|
|
||||||
|
// shallow copy is good enough, as slices fields are currently not updated
|
||||||
|
return slices.Clone(d.nsGroupStates)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Status) GetResolvedDomainsStates() map[domain.Domain]ResolvedDomainInfo {
|
func (d *Status) GetResolvedDomainsStates() map[domain.Domain]ResolvedDomainInfo {
|
||||||
|
|||||||
154
client/internal/peer/wg_watcher.go
Normal file
154
client/internal/peer/wg_watcher.go
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
wgHandshakePeriod = 3 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
wgHandshakeOvertime = 30 * time.Second // allowed delay in network
|
||||||
|
checkPeriod = wgHandshakePeriod + wgHandshakeOvertime
|
||||||
|
)
|
||||||
|
|
||||||
|
type WGInterfaceStater interface {
|
||||||
|
GetStats(key string) (configurer.WGStats, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WGWatcher struct {
|
||||||
|
log *log.Entry
|
||||||
|
wgIfaceStater WGInterfaceStater
|
||||||
|
peerKey string
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
ctxCancel context.CancelFunc
|
||||||
|
ctxLock sync.Mutex
|
||||||
|
waitGroup sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWGWatcher(log *log.Entry, wgIfaceStater WGInterfaceStater, peerKey string) *WGWatcher {
|
||||||
|
return &WGWatcher{
|
||||||
|
log: log,
|
||||||
|
wgIfaceStater: wgIfaceStater,
|
||||||
|
peerKey: peerKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableWgWatcher starts the WireGuard watcher. If it is already enabled, it will return immediately and do nothing.
|
||||||
|
func (w *WGWatcher) EnableWgWatcher(parentCtx context.Context, onDisconnectedFn func()) {
|
||||||
|
w.log.Debugf("enable WireGuard watcher")
|
||||||
|
w.ctxLock.Lock()
|
||||||
|
defer w.ctxLock.Unlock()
|
||||||
|
|
||||||
|
if w.ctx != nil && w.ctx.Err() == nil {
|
||||||
|
w.log.Errorf("WireGuard watcher already enabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, ctxCancel := context.WithCancel(parentCtx)
|
||||||
|
w.ctx = ctx
|
||||||
|
w.ctxCancel = ctxCancel
|
||||||
|
|
||||||
|
initialHandshake, err := w.wgState()
|
||||||
|
if err != nil {
|
||||||
|
w.log.Warnf("failed to read initial wg stats: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.waitGroup.Add(1)
|
||||||
|
go w.periodicHandshakeCheck(ctx, ctxCancel, onDisconnectedFn, initialHandshake)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableWgWatcher stops the WireGuard watcher and wait for the watcher to exit
|
||||||
|
func (w *WGWatcher) DisableWgWatcher() {
|
||||||
|
w.ctxLock.Lock()
|
||||||
|
defer w.ctxLock.Unlock()
|
||||||
|
|
||||||
|
if w.ctxCancel == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.log.Debugf("disable WireGuard watcher")
|
||||||
|
|
||||||
|
w.ctxCancel()
|
||||||
|
w.ctxCancel = nil
|
||||||
|
w.waitGroup.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// wgStateCheck help to check the state of the WireGuard handshake and relay connection
|
||||||
|
func (w *WGWatcher) periodicHandshakeCheck(ctx context.Context, ctxCancel context.CancelFunc, onDisconnectedFn func(), initialHandshake time.Time) {
|
||||||
|
w.log.Infof("WireGuard watcher started")
|
||||||
|
defer w.waitGroup.Done()
|
||||||
|
|
||||||
|
timer := time.NewTimer(wgHandshakeOvertime)
|
||||||
|
defer timer.Stop()
|
||||||
|
defer ctxCancel()
|
||||||
|
|
||||||
|
lastHandshake := initialHandshake
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
handshake, ok := w.handshakeCheck(lastHandshake)
|
||||||
|
if !ok {
|
||||||
|
onDisconnectedFn()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lastHandshake = *handshake
|
||||||
|
|
||||||
|
resetTime := time.Until(handshake.Add(checkPeriod))
|
||||||
|
timer.Reset(resetTime)
|
||||||
|
|
||||||
|
w.log.Debugf("WireGuard watcher reset timer: %v", resetTime)
|
||||||
|
case <-ctx.Done():
|
||||||
|
w.log.Infof("WireGuard watcher stopped")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handshakeCheck checks the WireGuard handshake and return the new handshake time if it is different from the previous one
|
||||||
|
func (w *WGWatcher) handshakeCheck(lastHandshake time.Time) (*time.Time, bool) {
|
||||||
|
handshake, err := w.wgState()
|
||||||
|
if err != nil {
|
||||||
|
w.log.Errorf("failed to read wg stats: %v", err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
w.log.Tracef("previous handshake, handshake: %v, %v", lastHandshake, handshake)
|
||||||
|
|
||||||
|
// the current know handshake did not change
|
||||||
|
if handshake.Equal(lastHandshake) {
|
||||||
|
w.log.Warnf("WireGuard handshake timed out, closing relay connection: %v", handshake)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// in case if the machine is suspended, the handshake time will be in the past
|
||||||
|
if handshake.Add(checkPeriod).Before(time.Now()) {
|
||||||
|
w.log.Warnf("WireGuard handshake timed out, closing relay connection: %v", handshake)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// error handling for handshake time in the future
|
||||||
|
if handshake.After(time.Now()) {
|
||||||
|
w.log.Warnf("WireGuard handshake is in the future, closing relay connection: %v", handshake)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return &handshake, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WGWatcher) wgState() (time.Time, error) {
|
||||||
|
wgState, err := w.wgIfaceStater.GetStats(w.peerKey)
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
return wgState.LastHandshake, nil
|
||||||
|
}
|
||||||
98
client/internal/peer/wg_watcher_test.go
Normal file
98
client/internal/peer/wg_watcher_test.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package peer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/netbirdio/netbird/client/iface/configurer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MocWgIface struct {
|
||||||
|
initial bool
|
||||||
|
lastHandshake time.Time
|
||||||
|
stop bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MocWgIface) GetStats(key string) (configurer.WGStats, error) {
|
||||||
|
if !m.initial {
|
||||||
|
m.initial = true
|
||||||
|
return configurer.WGStats{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !m.stop {
|
||||||
|
m.lastHandshake = time.Now()
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := configurer.WGStats{
|
||||||
|
LastHandshake: m.lastHandshake,
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MocWgIface) disconnect() {
|
||||||
|
m.stop = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWGWatcher_EnableWgWatcher(t *testing.T) {
|
||||||
|
checkPeriod = 5 * time.Second
|
||||||
|
wgHandshakeOvertime = 1 * time.Second
|
||||||
|
|
||||||
|
mlog := log.WithField("peer", "tet")
|
||||||
|
mocWgIface := &MocWgIface{}
|
||||||
|
watcher := NewWGWatcher(mlog, mocWgIface, "")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
onDisconnected := make(chan struct{}, 1)
|
||||||
|
watcher.EnableWgWatcher(ctx, func() {
|
||||||
|
mlog.Infof("onDisconnectedFn")
|
||||||
|
onDisconnected <- struct{}{}
|
||||||
|
})
|
||||||
|
|
||||||
|
// wait for initial reading
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
mocWgIface.disconnect()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-onDisconnected:
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Errorf("timeout")
|
||||||
|
}
|
||||||
|
watcher.DisableWgWatcher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWGWatcher_ReEnable(t *testing.T) {
|
||||||
|
checkPeriod = 5 * time.Second
|
||||||
|
wgHandshakeOvertime = 1 * time.Second
|
||||||
|
|
||||||
|
mlog := log.WithField("peer", "tet")
|
||||||
|
mocWgIface := &MocWgIface{}
|
||||||
|
watcher := NewWGWatcher(mlog, mocWgIface, "")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
onDisconnected := make(chan struct{}, 1)
|
||||||
|
|
||||||
|
watcher.EnableWgWatcher(ctx, func() {})
|
||||||
|
watcher.DisableWgWatcher()
|
||||||
|
|
||||||
|
watcher.EnableWgWatcher(ctx, func() {
|
||||||
|
onDisconnected <- struct{}{}
|
||||||
|
})
|
||||||
|
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
mocWgIface.disconnect()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-onDisconnected:
|
||||||
|
case <-time.After(10 * time.Second):
|
||||||
|
t.Errorf("timeout")
|
||||||
|
}
|
||||||
|
watcher.DisableWgWatcher()
|
||||||
|
}
|
||||||
@@ -31,20 +31,15 @@ type ICEConnInfo struct {
|
|||||||
RelayedOnLocal bool
|
RelayedOnLocal bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkerICECallbacks struct {
|
|
||||||
OnConnReady func(ConnPriority, ICEConnInfo)
|
|
||||||
OnStatusChanged func(ConnStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
type WorkerICE struct {
|
type WorkerICE struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
config ConnConfig
|
config ConnConfig
|
||||||
|
conn *Conn
|
||||||
signaler *Signaler
|
signaler *Signaler
|
||||||
iFaceDiscover stdnet.ExternalIFaceDiscover
|
iFaceDiscover stdnet.ExternalIFaceDiscover
|
||||||
statusRecorder *Status
|
statusRecorder *Status
|
||||||
hasRelayOnLocally bool
|
hasRelayOnLocally bool
|
||||||
conn WorkerICECallbacks
|
|
||||||
|
|
||||||
agent *ice.Agent
|
agent *ice.Agent
|
||||||
muxAgent sync.Mutex
|
muxAgent sync.Mutex
|
||||||
@@ -60,16 +55,16 @@ type WorkerICE struct {
|
|||||||
lastKnownState ice.ConnectionState
|
lastKnownState ice.ConnectionState
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWorkerICE(ctx context.Context, log *log.Entry, config ConnConfig, signaler *Signaler, ifaceDiscover stdnet.ExternalIFaceDiscover, statusRecorder *Status, hasRelayOnLocally bool, callBacks WorkerICECallbacks) (*WorkerICE, error) {
|
func NewWorkerICE(ctx context.Context, log *log.Entry, config ConnConfig, conn *Conn, signaler *Signaler, ifaceDiscover stdnet.ExternalIFaceDiscover, statusRecorder *Status, hasRelayOnLocally bool) (*WorkerICE, error) {
|
||||||
w := &WorkerICE{
|
w := &WorkerICE{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
log: log,
|
log: log,
|
||||||
config: config,
|
config: config,
|
||||||
|
conn: conn,
|
||||||
signaler: signaler,
|
signaler: signaler,
|
||||||
iFaceDiscover: ifaceDiscover,
|
iFaceDiscover: ifaceDiscover,
|
||||||
statusRecorder: statusRecorder,
|
statusRecorder: statusRecorder,
|
||||||
hasRelayOnLocally: hasRelayOnLocally,
|
hasRelayOnLocally: hasRelayOnLocally,
|
||||||
conn: callBacks,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
localUfrag, localPwd, err := icemaker.GenerateICECredentials()
|
localUfrag, localPwd, err := icemaker.GenerateICECredentials()
|
||||||
@@ -154,8 +149,8 @@ func (w *WorkerICE) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
|
|||||||
Relayed: isRelayed(pair),
|
Relayed: isRelayed(pair),
|
||||||
RelayedOnLocal: isRelayCandidate(pair.Local),
|
RelayedOnLocal: isRelayCandidate(pair.Local),
|
||||||
}
|
}
|
||||||
w.log.Debugf("on ICE conn read to use ready")
|
w.log.Debugf("on ICE conn is ready to use")
|
||||||
go w.conn.OnConnReady(selectedPriority(pair), ci)
|
go w.conn.onICEConnectionIsReady(selectedPriority(pair), ci)
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnRemoteCandidate Handles ICE connection Candidate provided by the remote peer.
|
// OnRemoteCandidate Handles ICE connection Candidate provided by the remote peer.
|
||||||
@@ -220,7 +215,7 @@ func (w *WorkerICE) reCreateAgent(agentCancel context.CancelFunc, candidates []i
|
|||||||
case ice.ConnectionStateFailed, ice.ConnectionStateDisconnected:
|
case ice.ConnectionStateFailed, ice.ConnectionStateDisconnected:
|
||||||
if w.lastKnownState != ice.ConnectionStateDisconnected {
|
if w.lastKnownState != ice.ConnectionStateDisconnected {
|
||||||
w.lastKnownState = ice.ConnectionStateDisconnected
|
w.lastKnownState = ice.ConnectionStateDisconnected
|
||||||
w.conn.OnStatusChanged(StatusDisconnected)
|
w.conn.onICEStateDisconnected()
|
||||||
}
|
}
|
||||||
w.closeAgent(agentCancel)
|
w.closeAgent(agentCancel)
|
||||||
default:
|
default:
|
||||||
@@ -255,6 +250,10 @@ func (w *WorkerICE) closeAgent(cancel context.CancelFunc) {
|
|||||||
defer w.muxAgent.Unlock()
|
defer w.muxAgent.Unlock()
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
|
if w.agent == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := w.agent.Close(); err != nil {
|
if err := w.agent.Close(); err != nil {
|
||||||
w.log.Warnf("failed to close ICE agent: %s", err)
|
w.log.Warnf("failed to close ICE agent: %s", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,52 +6,41 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
relayClient "github.com/netbirdio/netbird/relay/client"
|
relayClient "github.com/netbirdio/netbird/relay/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
wgHandshakePeriod = 3 * time.Minute
|
|
||||||
wgHandshakeOvertime = 30 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
type RelayConnInfo struct {
|
type RelayConnInfo struct {
|
||||||
relayedConn net.Conn
|
relayedConn net.Conn
|
||||||
rosenpassPubKey []byte
|
rosenpassPubKey []byte
|
||||||
rosenpassAddr string
|
rosenpassAddr string
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkerRelayCallbacks struct {
|
|
||||||
OnConnReady func(RelayConnInfo)
|
|
||||||
OnDisconnected func()
|
|
||||||
}
|
|
||||||
|
|
||||||
type WorkerRelay struct {
|
type WorkerRelay struct {
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
isController bool
|
isController bool
|
||||||
config ConnConfig
|
config ConnConfig
|
||||||
|
conn *Conn
|
||||||
relayManager relayClient.ManagerService
|
relayManager relayClient.ManagerService
|
||||||
callBacks WorkerRelayCallbacks
|
|
||||||
|
|
||||||
relayedConn net.Conn
|
relayedConn net.Conn
|
||||||
relayLock sync.Mutex
|
relayLock sync.Mutex
|
||||||
ctxWgWatch context.Context
|
|
||||||
ctxCancelWgWatch context.CancelFunc
|
|
||||||
ctxLock sync.Mutex
|
|
||||||
|
|
||||||
relaySupportedOnRemotePeer atomic.Bool
|
relaySupportedOnRemotePeer atomic.Bool
|
||||||
|
|
||||||
|
wgWatcher *WGWatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWorkerRelay(log *log.Entry, ctrl bool, config ConnConfig, relayManager relayClient.ManagerService, callbacks WorkerRelayCallbacks) *WorkerRelay {
|
func NewWorkerRelay(log *log.Entry, ctrl bool, config ConnConfig, conn *Conn, relayManager relayClient.ManagerService) *WorkerRelay {
|
||||||
r := &WorkerRelay{
|
r := &WorkerRelay{
|
||||||
log: log,
|
log: log,
|
||||||
isController: ctrl,
|
isController: ctrl,
|
||||||
config: config,
|
config: config,
|
||||||
|
conn: conn,
|
||||||
relayManager: relayManager,
|
relayManager: relayManager,
|
||||||
callBacks: callbacks,
|
wgWatcher: NewWGWatcher(log, config.WgConfig.WgInterface, config.Key),
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
@@ -87,7 +76,7 @@ func (w *WorkerRelay) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
|
|||||||
w.relayedConn = relayedConn
|
w.relayedConn = relayedConn
|
||||||
w.relayLock.Unlock()
|
w.relayLock.Unlock()
|
||||||
|
|
||||||
err = w.relayManager.AddCloseListener(srv, w.onRelayMGDisconnected)
|
err = w.relayManager.AddCloseListener(srv, w.onRelayClientDisconnected)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("failed to add close listener: %s", err)
|
log.Errorf("failed to add close listener: %s", err)
|
||||||
_ = relayedConn.Close()
|
_ = relayedConn.Close()
|
||||||
@@ -95,7 +84,7 @@ func (w *WorkerRelay) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.log.Debugf("peer conn opened via Relay: %s", srv)
|
w.log.Debugf("peer conn opened via Relay: %s", srv)
|
||||||
go w.callBacks.OnConnReady(RelayConnInfo{
|
go w.conn.onRelayConnectionIsReady(RelayConnInfo{
|
||||||
relayedConn: relayedConn,
|
relayedConn: relayedConn,
|
||||||
rosenpassPubKey: remoteOfferAnswer.RosenpassPubKey,
|
rosenpassPubKey: remoteOfferAnswer.RosenpassPubKey,
|
||||||
rosenpassAddr: remoteOfferAnswer.RosenpassAddr,
|
rosenpassAddr: remoteOfferAnswer.RosenpassAddr,
|
||||||
@@ -103,32 +92,11 @@ func (w *WorkerRelay) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkerRelay) EnableWgWatcher(ctx context.Context) {
|
func (w *WorkerRelay) EnableWgWatcher(ctx context.Context) {
|
||||||
w.log.Debugf("enable WireGuard watcher")
|
w.wgWatcher.EnableWgWatcher(ctx, w.onWGDisconnected)
|
||||||
w.ctxLock.Lock()
|
|
||||||
defer w.ctxLock.Unlock()
|
|
||||||
|
|
||||||
if w.ctxWgWatch != nil && w.ctxWgWatch.Err() == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, ctxCancel := context.WithCancel(ctx)
|
|
||||||
w.ctxWgWatch = ctx
|
|
||||||
w.ctxCancelWgWatch = ctxCancel
|
|
||||||
|
|
||||||
w.wgStateCheck(ctx, ctxCancel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkerRelay) DisableWgWatcher() {
|
func (w *WorkerRelay) DisableWgWatcher() {
|
||||||
w.ctxLock.Lock()
|
w.wgWatcher.DisableWgWatcher()
|
||||||
defer w.ctxLock.Unlock()
|
|
||||||
|
|
||||||
if w.ctxCancelWgWatch == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.log.Debugf("disable WireGuard watcher")
|
|
||||||
|
|
||||||
w.ctxCancelWgWatch()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkerRelay) RelayInstanceAddress() (string, error) {
|
func (w *WorkerRelay) RelayInstanceAddress() (string, error) {
|
||||||
@@ -150,57 +118,17 @@ func (w *WorkerRelay) CloseConn() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := w.relayedConn.Close()
|
if err := w.relayedConn.Close(); err != nil {
|
||||||
if err != nil {
|
|
||||||
w.log.Warnf("failed to close relay connection: %v", err)
|
w.log.Warnf("failed to close relay connection: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wgStateCheck help to check the state of the WireGuard handshake and relay connection
|
func (w *WorkerRelay) onWGDisconnected() {
|
||||||
func (w *WorkerRelay) wgStateCheck(ctx context.Context, ctxCancel context.CancelFunc) {
|
|
||||||
w.log.Debugf("WireGuard watcher started")
|
|
||||||
lastHandshake, err := w.wgState()
|
|
||||||
if err != nil {
|
|
||||||
w.log.Warnf("failed to read wg stats: %v", err)
|
|
||||||
lastHandshake = time.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
go func(lastHandshake time.Time) {
|
|
||||||
timer := time.NewTimer(wgHandshakeOvertime)
|
|
||||||
defer timer.Stop()
|
|
||||||
defer ctxCancel()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
handshake, err := w.wgState()
|
|
||||||
if err != nil {
|
|
||||||
w.log.Errorf("failed to read wg stats: %v", err)
|
|
||||||
timer.Reset(wgHandshakeOvertime)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
w.log.Tracef("previous handshake, handshake: %v, %v", lastHandshake, handshake)
|
|
||||||
|
|
||||||
if handshake.Equal(lastHandshake) {
|
|
||||||
w.log.Infof("WireGuard handshake timed out, closing relay connection: %v", handshake)
|
|
||||||
w.relayLock.Lock()
|
w.relayLock.Lock()
|
||||||
_ = w.relayedConn.Close()
|
_ = w.relayedConn.Close()
|
||||||
w.relayLock.Unlock()
|
w.relayLock.Unlock()
|
||||||
w.callBacks.OnDisconnected()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resetTime := time.Until(handshake.Add(wgHandshakePeriod + wgHandshakeOvertime))
|
|
||||||
lastHandshake = handshake
|
|
||||||
timer.Reset(resetTime)
|
|
||||||
case <-ctx.Done():
|
|
||||||
w.log.Debugf("WireGuard watcher stopped")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(lastHandshake)
|
|
||||||
|
|
||||||
|
w.conn.onRelayDisconnected()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkerRelay) isRelaySupported(answer *OfferAnswer) bool {
|
func (w *WorkerRelay) isRelaySupported(answer *OfferAnswer) bool {
|
||||||
@@ -217,20 +145,7 @@ func (w *WorkerRelay) preferredRelayServer(myRelayAddress, remoteRelayAddress st
|
|||||||
return remoteRelayAddress
|
return remoteRelayAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WorkerRelay) wgState() (time.Time, error) {
|
func (w *WorkerRelay) onRelayClientDisconnected() {
|
||||||
wgState, err := w.config.WgConfig.WgInterface.GetStats(w.config.Key)
|
w.wgWatcher.DisableWgWatcher()
|
||||||
if err != nil {
|
go w.conn.onRelayDisconnected()
|
||||||
return time.Time{}, err
|
|
||||||
}
|
|
||||||
return wgState.LastHandshake, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *WorkerRelay) onRelayMGDisconnected() {
|
|
||||||
w.ctxLock.Lock()
|
|
||||||
defer w.ctxLock.Unlock()
|
|
||||||
|
|
||||||
if w.ctxCancelWgWatch != nil {
|
|
||||||
w.ctxCancelWgWatch()
|
|
||||||
}
|
|
||||||
go w.callBacks.OnDisconnected()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type ProbeHolder struct {
|
|
||||||
MgmProbe *Probe
|
|
||||||
SignalProbe *Probe
|
|
||||||
RelayProbe *Probe
|
|
||||||
WgProbe *Probe
|
|
||||||
}
|
|
||||||
|
|
||||||
// Probe allows to run on-demand callbacks from different code locations.
|
|
||||||
// Pass the probe to a receiving and a sending end. The receiving end starts listening
|
|
||||||
// to requests with Receive and executes a callback when the sending end requests it
|
|
||||||
// by calling Probe.
|
|
||||||
type Probe struct {
|
|
||||||
request chan struct{}
|
|
||||||
result chan bool
|
|
||||||
ready bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewProbe returns a new initialized probe.
|
|
||||||
func NewProbe() *Probe {
|
|
||||||
return &Probe{
|
|
||||||
request: make(chan struct{}),
|
|
||||||
result: make(chan bool),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Probe requests the callback to be run and returns a bool indicating success.
|
|
||||||
// It always returns true as long as the receiver is not ready.
|
|
||||||
func (p *Probe) Probe() bool {
|
|
||||||
if !p.ready {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
p.request <- struct{}{}
|
|
||||||
return <-p.result
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receive starts listening for probe requests. On such a request it runs the supplied
|
|
||||||
// callback func which must return a bool indicating success.
|
|
||||||
// Blocks until the passed context is cancelled.
|
|
||||||
func (p *Probe) Receive(ctx context.Context, callback func() bool) {
|
|
||||||
p.ready = true
|
|
||||||
defer func() {
|
|
||||||
p.ready = false
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-p.request:
|
|
||||||
p.result <- callback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user