Compare commits

..

2 Commits

Author SHA1 Message Date
mlsmaycon
3b32ca3924 Prepare SysV tooling in install-script test containers
The netbird package postinstall registers a SysV service when systemd is
not running. The trixie and fedora images ship without /etc/init.d and
the service tool, failing the postinstall before the UI gating under test.
2026-07-28 18:52:48 +02:00
mlsmaycon
6175888bce [misc] add dependencies to install.sh
- update workflow to test different containers
2026-07-28 11:37:21 +02:00
24 changed files with 180 additions and 44 deletions

View File

@@ -7,6 +7,7 @@ on:
pull_request:
paths:
- "release_files/install.sh"
- ".github/workflows/install-script-test.yml"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
cancel-in-progress: true
@@ -37,3 +38,64 @@ jobs:
- name: check cli binary
run: command -v netbird
test-install-script-distros:
name: UI gating on ${{ matrix.image }}${{ matrix.install_epel && ' (EPEL pre-confirmed)' || '' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
max-parallel: 3
matrix:
include:
# GTK 4 below 4.10: desktop app must be skipped, CLI installed
- image: "ubuntu:22.04"
expect_ui: false
- image: "debian:12"
expect_ui: false
# GTK 4.10+ and WebKitGTK 6.0 available: desktop app installed
- image: "debian:13"
expect_ui: true
- image: "fedora:43"
expect_ui: true
# webkitgtk6.0 needs EPEL: skipped without confirmation,
# installed when NETBIRD_INSTALL_EPEL=true
- image: "almalinux:10"
expect_ui: false
- image: "almalinux:10"
expect_ui: true
install_epel: true
container:
image: ${{ matrix.image }}
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: prepare container
# The netbird package postinstall registers a SysV service when
# systemd is not running. The trixie and fedora images ship without
# the SysV directories and tools it needs, unlike the older images.
run: |
mkdir -p /etc/init.d
if command -v dnf >/dev/null 2>&1; then
dnf -y install initscripts-service || dnf -y install initscripts
fi
- name: run install script
env:
XDG_CURRENT_DESKTOP: GNOME
NETBIRD_INSTALL_EPEL: ${{ matrix.install_epel && 'true' || '' }}
run: sh -x release_files/install.sh
- name: check binaries
run: |
command -v netbird
if [ "${{ matrix.expect_ui }}" = "true" ]; then
command -v netbird-ui
else
if command -v netbird-ui; then
echo "netbird-ui should not have been installed on ${{ matrix.image }}"
exit 1
fi
fi

View File

@@ -21,8 +21,8 @@ import (
"github.com/netbirdio/netbird/client/internal"
"github.com/netbirdio/netbird/client/internal/peer"
"github.com/netbirdio/netbird/client/internal/profilemanager"
nbnet "github.com/netbirdio/netbird/client/net"
"github.com/netbirdio/netbird/client/proto"
nbnet "github.com/netbirdio/netbird/client/net"
"github.com/netbirdio/netbird/client/server"
"github.com/netbirdio/netbird/client/system"
"github.com/netbirdio/netbird/shared/management/domain"

View File

@@ -702,6 +702,7 @@ func ifname(n string) []byte {
return b
}
// ipToBytes converts net.IP to the correct byte length for the address family.
func ipToBytes(ip net.IP, af addrFamily) []byte {
if af.addrLen == 4 {
@@ -709,3 +710,4 @@ func ipToBytes(ip net.IP, af addrFamily) []byte {
}
return ip.To16()
}

View File

@@ -158,8 +158,8 @@ func TestTCPPerStateTeardownTimeouts(t *testing.T) {
name: "FinWait2",
drive: func(t *testing.T, tr *TCPTracker, srcIP netip.Addr, srcPort uint16) (ConnKey, TCPState) {
establishConnection(t, tr, srcIP, dstIP, srcPort, dstPort)
tr.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck, 0) // FinWait1
require.True(t, tr.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck, 0)) // → FinWait2
tr.TrackOutbound(srcIP, dstIP, srcPort, dstPort, TCPFin|TCPAck, 0) // FinWait1
require.True(t, tr.IsValidInbound(dstIP, srcIP, dstPort, srcPort, TCPAck, 0)) // → FinWait2
return ConnKey{SrcIP: srcIP, DstIP: dstIP, SrcPort: srcPort, DstPort: dstPort}, TCPStateFinWait2
},
},

View File

@@ -18,7 +18,7 @@ type PeerRule struct {
protoLayer gopacket.LayerType
sPort *firewall.Port
dPort *firewall.Port
drop bool
drop bool
}
// ID returns the rule id

View File

@@ -34,14 +34,14 @@ func (f *fakeTunDevice) Create() (device.WGConfigurer, error) {
func (f *fakeTunDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
return nil, errors.New("not implemented")
}
func (f *fakeTunDevice) UpdateAddr(wgaddr.Address) error { return nil }
func (f *fakeTunDevice) WgAddress() wgaddr.Address { return wgaddr.Address{} }
func (f *fakeTunDevice) MTU() uint16 { return DefaultMTU }
func (f *fakeTunDevice) DeviceName() string { return "nb-close-test" }
func (f *fakeTunDevice) UpdateAddr(wgaddr.Address) error { return nil }
func (f *fakeTunDevice) WgAddress() wgaddr.Address { return wgaddr.Address{} }
func (f *fakeTunDevice) MTU() uint16 { return DefaultMTU }
func (f *fakeTunDevice) DeviceName() string { return "nb-close-test" }
func (f *fakeTunDevice) FilteredDevice() *device.FilteredDevice { return nil }
func (f *fakeTunDevice) Device() *wgdevice.Device { return nil }
func (f *fakeTunDevice) GetNet() *netstack.Net { return nil }
func (f *fakeTunDevice) GetICEBind() device.EndpointManager { return nil }
func (f *fakeTunDevice) Device() *wgdevice.Device { return nil }
func (f *fakeTunDevice) GetNet() *netstack.Net { return nil }
func (f *fakeTunDevice) GetICEBind() device.EndpointManager { return nil }
func (f *fakeTunDevice) Close() error {
close(f.closeStarted)

View File

@@ -116,11 +116,11 @@ func (d *DefaultManager) ApplyFiltering(networkMap *mgmProto.NetworkMap, dnsRout
// firewall state, so an identical hash means an identical resulting ruleset.
func (d *DefaultManager) firewallConfigHash(networkMap *mgmProto.NetworkMap, dnsRouteFeatureFlag bool) (uint64, error) {
return hashstructure.Hash(struct {
PeerRules []*mgmProto.FirewallRule
PeerRulesIsEmpty bool
RouteRules []*mgmProto.RouteFirewallRule
RouteRulesIsEmpty bool
DNSRouteFeatureFlag bool
PeerRules []*mgmProto.FirewallRule
PeerRulesIsEmpty bool
RouteRules []*mgmProto.RouteFirewallRule
RouteRulesIsEmpty bool
DNSRouteFeatureFlag bool
}{
PeerRules: networkMap.GetFirewallRules(),
PeerRulesIsEmpty: networkMap.GetFirewallRulesIsEmpty(),
@@ -407,6 +407,7 @@ func (d *DefaultManager) getRuleGroupingSelector(rule *mgmProto.FirewallRule) st
return fmt.Sprintf("%v:%v:%v:%s:%v", strconv.Itoa(int(rule.Direction)), rule.Action, rule.Protocol, rule.Port, rule.PortInfo)
}
// extractRuleIP extracts the peer IP from a firewall rule.
// If sourcePrefixes is populated (new management), decode the first entry and use its address.
// Otherwise fall back to the deprecated PeerIP string field (old management).

View File

@@ -556,7 +556,7 @@ func TestApplyFilteringSkipsUnchangedConfig(t *testing.T) {
func buildNetworkMap(peerRules, routeRules int) *mgmProto.NetworkMap {
nm := &mgmProto.NetworkMap{
FirewallRulesIsEmpty: peerRules == 0,
FirewallRulesIsEmpty: peerRules == 0,
RoutesFirewallRulesIsEmpty: routeRules == 0,
}
for i := range peerRules {

View File

@@ -49,3 +49,4 @@ func (tf *GeneralManager) FreeDNSFwd() error {
log.Debugf("free ebpf DNS forwarder")
return tf.unsetFeatureFlag(featureFlagDnsForwarder)
}

View File

@@ -138,6 +138,7 @@ func TestManager_Cleanup_NilMapping(t *testing.T) {
m.cleanup(context.Background(), gateway)
}
func TestManager_CreateMapping_PermanentLeaseFallback(t *testing.T) {
m := NewManager()
m.wgPort = 51820

View File

@@ -193,3 +193,4 @@ func buildAddressList(hostname string, remote net.Addr) []string {
}
return addresses
}

View File

@@ -11,7 +11,6 @@ package main
// - The in-process StatusNotifierWatcher + XEmbed host used on minimal WMs
// (Fluxbox, i3, dwm, OpenBox) maps a Button1 press to that same Activate
// call itself (xembed_host_linux.go), so it routes through the same hook.
//
// Registering OnClick here therefore covers both paths with one handler — no
// changes to the watcher or XEmbed C code are needed. Left-click now opens the
// main window; right-click still opens the menu via Wails' default

View File

@@ -72,12 +72,12 @@ type proxyConnection struct {
// Go-side callbacks exposed to JS. js.FuncOf pins the Go closure in a
// global handle map and MUST be released, otherwise every connection
// leaks the Go memory the closure captures.
wsHandlerFn js.Func
onMessageFn js.Func
onCloseFn js.Func
cleanupOnce sync.Once
ctx context.Context
cancel context.CancelFunc
wsHandlerFn js.Func
onMessageFn js.Func
onCloseFn js.Func
cleanupOnce sync.Once
ctx context.Context
cancel context.CancelFunc
}
// NewRDCleanPathProxy creates a new RDCleanPath proxy

View File

@@ -3,9 +3,9 @@ package controller
import (
"context"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
"github.com/netbirdio/netbird/management/internals/modules/zones"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
"github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/management/server/types"

View File

@@ -24,13 +24,13 @@ import (
"github.com/netbirdio/netbird/encryption"
"github.com/netbirdio/netbird/formatter/hook"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs"
accesslogsmanager "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs/manager"
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
nbgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
"github.com/netbirdio/netbird/management/server/activity"
activitystore "github.com/netbirdio/netbird/management/server/activity/store"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
nbcache "github.com/netbirdio/netbird/management/server/cache"
nbContext "github.com/netbirdio/netbird/management/server/context"
nbhttp "github.com/netbirdio/netbird/management/server/http"

View File

@@ -8,7 +8,6 @@ import (
"github.com/netbirdio/management-integrations/integrations"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
"github.com/netbirdio/netbird/management/internals/modules/peers"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/domain/manager"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
@@ -21,6 +20,7 @@ import (
recordsManager "github.com/netbirdio/netbird/management/internals/modules/zones/records/manager"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/account"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
"github.com/netbirdio/netbird/management/server/geolocation"
"github.com/netbirdio/netbird/management/server/groups"
"github.com/netbirdio/netbird/management/server/idp"

View File

@@ -29,7 +29,6 @@ import (
"github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
"github.com/netbirdio/netbird/management/internals/modules/peers"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/accesslogs"
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/proxy"
@@ -37,6 +36,7 @@ import (
"github.com/netbirdio/netbird/management/internals/modules/reverseproxy/sessionkey"
"github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/peer"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
"github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/management/server/users"
proxyauth "github.com/netbirdio/netbird/proxy/auth"

View File

@@ -8,8 +8,8 @@ import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/netip"
"net/http/httptest"
"strings"
"testing"

View File

@@ -191,3 +191,4 @@ func TestPolicyRuleEqual_EmptySlices(t *testing.T) {
}
assert.True(t, a.Equal(b))
}

View File

@@ -27,12 +27,12 @@ func TestOpenAIDetectFromURL(t *testing.T) {
// catches Cloudflare's OpenAI direct path
// (/v1/{account}/{gateway}/openai/chat/completions) and
// compat path (/v1/{account}/{gateway}/compat/chat/completions).
"/v1/{account}/{gateway}/openai/chat/completions": true,
"/v1/{account}/{gateway}/compat/chat/completions": true,
"/chat/completions": true,
"/v1/messages": false,
"/healthz": false,
"": false,
"/v1/{account}/{gateway}/openai/chat/completions": true,
"/v1/{account}/{gateway}/compat/chat/completions": true,
"/chat/completions": true,
"/v1/messages": false,
"/healthz": false,
"": false,
}
for path, want := range cases {
assert.Equal(t, want, p.DetectFromURL(path), "DetectFromURL(%q)", path)

View File

@@ -14,9 +14,9 @@ import (
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/test/bufconn"
mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
"github.com/netbirdio/netbird/management/server/store"
nbtypes "github.com/netbirdio/netbird/management/server/types"
"github.com/netbirdio/netbird/proxy/internal/middleware"

View File

@@ -18,10 +18,10 @@ import (
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/test/bufconn"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
rpservice "github.com/netbirdio/netbird/management/internals/modules/reverseproxy/service"
mgmtgrpc "github.com/netbirdio/netbird/management/internals/shared/grpc"
"github.com/netbirdio/netbird/management/internals/modules/agentnetwork"
agentNetworkTypes "github.com/netbirdio/netbird/management/internals/modules/agentnetwork/types"
"github.com/netbirdio/netbird/management/server/store"
"github.com/netbirdio/netbird/proxy/internal/middleware"
"github.com/netbirdio/netbird/proxy/internal/middleware/bodytap"

View File

@@ -1964,3 +1964,4 @@ func TestRouter_Serve_BacksOffOnTransientError(t *testing.T) {
type errSentinel string
func (e errSentinel) Error() string { return string(e) }

View File

@@ -134,6 +134,72 @@ repo_gpgcheck=1
EOF
}
# The desktop app renders in a GTK 4 WebKit webview and needs GTK 4.10+ with
# WebKitGTK 6.0. The netbird-ui packages do not declare these libraries, so
# the installer resolves them and degrades to a CLI-only install when the
# system cannot provide them.
UI_MIN_GTK_VERSION="4.10"
skip_ui_app() {
SKIP_UI_APP=true
echo "NetBird UI installation will be omitted: $1"
}
install_ui_apt() {
GTK_CANDIDATE="$(apt-cache policy libgtk-4-1 2>/dev/null | awk '/Candidate:/ {print $2}')"
if [ -z "$GTK_CANDIDATE" ] || [ "$GTK_CANDIDATE" = "(none)" ]; then
skip_ui_app "GTK 4 is not available in the configured APT repositories"
return 0
fi
if ! dpkg --compare-versions "$GTK_CANDIDATE" ge "$UI_MIN_GTK_VERSION"; then
skip_ui_app "the desktop app needs GTK ${UI_MIN_GTK_VERSION}+ and this release provides ${GTK_CANDIDATE} (Ubuntu 24.04+ or Debian 13+ required)"
return 0
fi
if ! ${SUDO} apt-get install netbird-ui libgtk-4-1 libwebkitgtk-6.0-4 xdg-utils -y; then
skip_ui_app "the desktop app dependencies could not be installed"
fi
}
# Enabling EPEL changes the system's repository configuration, so it needs
# an explicit go-ahead: either NETBIRD_INSTALL_EPEL=true in the environment
# or an interactive confirmation. Returns 0 when EPEL is available.
confirm_epel() {
if rpm -q epel-release >/dev/null 2>&1; then
return 0
fi
if [ "${NETBIRD_INSTALL_EPEL:-}" = "true" ]; then
echo "NETBIRD_INSTALL_EPEL=true is set, enabling the EPEL repository"
${SUDO} dnf -y install epel-release
return $?
fi
if (exec < /dev/tty) 2>/dev/null; then
printf "The desktop app needs WebKitGTK 6.0 from the EPEL repository (https://docs.fedoraproject.org/en-US/epel/). Enable EPEL and continue? [y/N] "
read -r EPEL_REPLY < /dev/tty
case "$EPEL_REPLY" in
y|Y|yes|YES)
${SUDO} dnf -y install epel-release
return $?
;;
esac
fi
return 1
}
install_ui_dnf() {
# webkitgtk6.0 comes from EPEL on RHEL, AlmaLinux and Rocky Linux 10.
case "$OS_NAME" in
rhel|almalinux|rocky|centos)
if ! confirm_epel; then
skip_ui_app "the desktop app needs WebKitGTK 6.0 from EPEL; re-run with NETBIRD_INSTALL_EPEL=true to enable it"
return 0
fi
;;
esac
if ! ${SUDO} dnf -y install netbird-ui gtk4 webkitgtk6.0 xdg-utils; then
skip_ui_app "GTK 4.10+ and WebKitGTK 6.0 are not available on this system (Fedora 43+ or RHEL 10+ required)"
fi
}
prepare_tun_module() {
# Create the necessary file structure for /dev/net/tun
if [ ! -c /dev/net/tun ]; then
@@ -226,14 +292,16 @@ install_netbird() {
${SUDO} apt-get install netbird -y
if ! $SKIP_UI_APP; then
${SUDO} apt-get install netbird-ui -y
install_ui_apt
fi
;;
yum)
add_rpm_repo
${SUDO} yum -y install netbird
if ! $SKIP_UI_APP; then
${SUDO} yum -y install netbird-ui
# Systems where dnf is absent (RHEL 9, Amazon Linux) do not
# provide GTK 4.10+ or WebKitGTK 6.0.
skip_ui_app "this system does not provide the desktop app dependencies (RHEL 10+ required)"
fi
;;
dnf)
@@ -241,7 +309,7 @@ install_netbird() {
${SUDO} dnf -y install netbird
if ! $SKIP_UI_APP; then
${SUDO} dnf -y install netbird-ui
install_ui_dnf
fi
;;
rpm-ostree)
@@ -401,9 +469,8 @@ if type uname >/dev/null 2>&1; then
OS_NAME="$(. /etc/os-release && echo "$ID")"
INSTALL_DIR="/usr/bin"
# Allow netbird UI installation for x64 arch only
if [ "$ARCH" != "amd64" ] && [ "$ARCH" != "arm64" ] \
&& [ "$ARCH" != "x86_64" ];then
# The netbird-ui Linux packages are built for x86_64 only
if [ "$ARCH" != "amd64" ] && [ "$ARCH" != "x86_64" ];then
SKIP_UI_APP=true
echo "NetBird UI installation will be omitted as $ARCH is not a compatible architecture"
fi