diff --git a/README.md b/README.md
index 4273998..63e538f 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
![goroutines](https://img.shields.io/badge/go%20routines-not%20leaking-success)
![file descriptors](https://img.shields.io/badge/file%20descriptors-not%20leaking-success)
[![Go Report Card](https://goreportcard.com/badge/github.com/siemens/ghostwire/v2)](https://goreportcard.com/report/github.com/siemens/ghostwire/v2)
-![Coverage](https://img.shields.io/badge/Coverage-77.0%25-yellow)
+![Coverage](https://img.shields.io/badge/Coverage-76.9%25-yellow)
**G(h)ostwire** discovers the virtual (or not) network configuration inside
_Linux_ hosts – and can be deployed as a REST service or consumed as a Go
diff --git a/go.mod b/go.mod
index d0fc82c..368f29c 100644
--- a/go.mod
+++ b/go.mod
@@ -7,7 +7,7 @@ replace github.com/mattn/go-sqlite3 => github.com/mattn/go-sqlite3 v1.14.12
require (
github.com/cenkalti/backoff/v4 v4.3.0
github.com/containernetworking/cni v1.2.3
- github.com/docker/docker v27.1.0+incompatible
+ github.com/docker/docker v27.1.1+incompatible
github.com/dustinkirkland/golang-petname v0.0.0-20240428194347-eebcea082ee0
github.com/getkin/kin-openapi v0.126.0
github.com/google/nftables v0.2.1-0.20240422065334-aa8348f7904c
@@ -29,7 +29,7 @@ require (
github.com/thediveo/go-plugger/v3 v3.1.0
github.com/thediveo/ioctl v0.9.3
github.com/thediveo/lxkns v0.36.0
- github.com/thediveo/morbyd v0.13.0
+ github.com/thediveo/morbyd v0.13.1
github.com/thediveo/namspill v0.1.6
github.com/thediveo/netdb v1.1.2
github.com/thediveo/notwork v1.6.2
diff --git a/go.sum b/go.sum
index cd8ea75..f3edfb9 100644
--- a/go.sum
+++ b/go.sum
@@ -63,8 +63,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v25.0.4+incompatible h1:DatRkJ+nrFoYL2HZUzjM5Z5sAmcA5XGp+AW0oEw2+cA=
github.com/docker/cli v25.0.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
-github.com/docker/docker v27.1.0+incompatible h1:rEHVQc4GZ0MIQKifQPHSFGV/dVgaZafgRf8fCPtDYBs=
-github.com/docker/docker v27.1.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v27.1.1+incompatible h1:hO/M4MtV36kzKldqnA37IWhebRA+LnqqcqDja6kVaKY=
+github.com/docker/docker v27.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
@@ -302,8 +302,8 @@ github.com/thediveo/ioctl v0.9.3 h1:DCxyUUY15z/Zezz+wf2nlbVf3yFh0nvfM7i7KnfgG8s=
github.com/thediveo/ioctl v0.9.3/go.mod h1:Ro3WW0UuPDh1QByEwNb/alva3ODM+GbRlb80u/LZU9o=
github.com/thediveo/lxkns v0.36.0 h1:2UrV8WKs2C9uKscHxAyw0M5u3y9eop8wsZrBAlqitbw=
github.com/thediveo/lxkns v0.36.0/go.mod h1:zYPNiNi6AK+ufDJYhivwn+OGj1hRHKF/uAEwuFpo+20=
-github.com/thediveo/morbyd v0.13.0 h1:85K8vKU/Af/KnakjhOSGvdVJojxTSlFMfwO6vvlZ5t8=
-github.com/thediveo/morbyd v0.13.0/go.mod h1:4g3wHYItuUdqIoIZmnOtifaI3X2PBqnCJpewBpLmlyY=
+github.com/thediveo/morbyd v0.13.1 h1:mDQ27NzPXD5WIZ5t79QQazcj0tFat6HmQhbMveJ6s/A=
+github.com/thediveo/morbyd v0.13.1/go.mod h1:4g3wHYItuUdqIoIZmnOtifaI3X2PBqnCJpewBpLmlyY=
github.com/thediveo/namspill v0.1.6 h1:eD8puqhwIkBS78vrzJtY46eurHX0o6JIAqzgkRmMLl0=
github.com/thediveo/namspill v0.1.6/go.mod h1:oRhr6rRg9z5pHuHckecgP4l9qN4YECZ22TtGs9Ma51E=
github.com/thediveo/netdb v1.1.2 h1:XdLx/YJPutxrSkPYtmCAIY5sgAvxtkS1Tz+Z0UX2I+U=
diff --git a/network/portfwd/docker/docker.go b/network/portfwd/docker/docker.go
index 7718714..d1ce181 100644
--- a/network/portfwd/docker/docker.go
+++ b/network/portfwd/docker/docker.go
@@ -36,6 +36,13 @@ func PortForwardings(tables nufftables.TableMap, family nufftables.TableFamily)
if nattable == nil {
return nil
}
+ return grabPortForwardings(nattable)
+}
+
+// grabPortForwardings is a convenience helper to wire up the individual port
+// forwarding detectors in a single place, making maintenance easier for both
+// PROD and TEST.
+func grabPortForwardings(nattable *nufftables.Table) []*portfinder.ForwardedPortRange {
forwardedPorts := forwardedPortsMk1(nattable)
forwardedPorts = append(forwardedPorts, forwardedPortsMk2(nattable)...)
forwardedPorts = append(forwardedPorts, forwardedPortsMk3(nattable)...)
@@ -69,7 +76,7 @@ func forwardedPortsInChainMk2(chain *nufftables.Chain) []*portfinder.ForwardedPo
family := chain.Table.Family
forwardedPorts := []*portfinder.ForwardedPortRange{}
for _, rule := range chain.Rules {
- exprs, proto := nftget.L4ProtoTcpUdp(rule.Exprs)
+ exprs, proto := nftget.MetaL4ProtoTcpUdp(rule.Exprs)
exprs, origIP := nftget.OptionalIPv46(exprs, family)
exprs, port := nufftables.OfTypeTransformed(exprs, nftget.Port)
exprs, dnat := dsl.TargetDNAT(exprs)
@@ -112,7 +119,7 @@ func forwardedPortsInChainMk3(chain *nufftables.Chain) []*portfinder.ForwardedPo
forwardedPorts := []*portfinder.ForwardedPortRange{}
for _, rule := range chain.Rules {
exprs, origIP := nftget.OptionalDestIPv46(rule.Exprs, family)
- exprs, proto := nftget.L4ProtoTcpUdp(exprs)
+ exprs, proto := nftget.PayloadL4ProtoTcpUdp(exprs)
exprs, port := nftget.PayloadPort(exprs)
exprs, dnat := dsl.TargetDNAT(exprs)
if exprs == nil || dnat.Flags&dnatWithIPsAndPorts != dnatWithIPsAndPorts || port == 0 {
diff --git a/network/portfwd/docker/docker_test.go b/network/portfwd/docker/docker_test.go
index cd6cb90..bef401c 100644
--- a/network/portfwd/docker/docker_test.go
+++ b/network/portfwd/docker/docker_test.go
@@ -18,20 +18,12 @@ import (
"github.com/thediveo/morbyd/session"
"github.com/thediveo/notwork/netns"
"github.com/thediveo/nufftables"
- "github.com/thediveo/nufftables/portfinder"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/thediveo/success"
)
-func fwports(nattable *nufftables.Table) []*portfinder.ForwardedPortRange {
- forwardedPorts := forwardedPortsMk1(nattable)
- forwardedPorts = append(forwardedPorts, forwardedPortsMk2(nattable)...)
- forwardedPorts = append(forwardedPorts, forwardedPortsMk3(nattable)...)
- return forwardedPorts
-}
-
var _ = Describe("Docker port forwarding", Ordered, func() {
var cntrPID int
@@ -76,7 +68,7 @@ var _ = Describe("Docker port forwarding", Ordered, func() {
nattable := tables.Table("nat", nufftables.TableFamilyIPv4)
Expect(nattable).NotTo(BeNil())
Expect(nattable.ChainsByName).NotTo(BeEmpty())
- forwardedPorts := fwports(nattable)
+ forwardedPorts := grabPortForwardings(nattable)
Expect(forwardedPorts).To(ContainElement(And(
HaveField("Protocol", "tcp"),
HaveField("IP", net.ParseIP("127.0.0.1").To4()),
@@ -103,8 +95,11 @@ var _ = Describe("Docker port forwarding", Ordered, func() {
nattable := tables.Table("nat", nufftables.TableFamilyIPv4)
Expect(nattable).NotTo(BeNil())
Expect(nattable.ChainsByName).NotTo(BeEmpty())
- forwardedPorts := fwports(nattable)
- Expect(forwardedPorts).To(ContainElements(
+ forwardedPorts := grabPortForwardings(nattable)
+ // Ensure to exactly match in order to catch any false positives; this
+ // is possible in this case because we're looking at the nft inside the
+ // container and thus know what should be there and what shouldn't.
+ Expect(forwardedPorts).To(ConsistOf(
And(
HaveField("Protocol", "tcp"),
HaveField("IP", net.ParseIP("127.0.0.11").To4()),
diff --git a/network/portfwd/nftget/l4proto.go b/network/portfwd/nftget/l4proto.go
index 6e0fc83..42851c6 100644
--- a/network/portfwd/nftget/l4proto.go
+++ b/network/portfwd/nftget/l4proto.go
@@ -10,13 +10,17 @@ import (
"golang.org/x/sys/unix"
)
-// L4ProtoTcpUdp returns the transport layer protocol name checked for from
-// either a Meta/Cmp twin-expression or a Payload/Cmp twin-expression, together
-// with the remaining expressions; otherwise, it returns nil.
-func L4ProtoTcpUdp(exprs nufftables.Expressions) (nufftables.Expressions, string) {
- if exprs, proto := nufftables.PrefixedOfTypeTransformed(exprs, isMetaL4Proto, TcpUdp); exprs != nil {
- return exprs, proto
- }
+// MetaL4ProtoTcpUdp returns the transport layer protocol name checked for from
+// a Meta/Cmp twin-expression, together with the remaining expressions;
+// otherwise, it returns nil.
+func MetaL4ProtoTcpUdp(exprs nufftables.Expressions) (nufftables.Expressions, string) {
+ return nufftables.PrefixedOfTypeTransformed(exprs, isMetaL4Proto, TcpUdp)
+}
+
+// PayloadL4ProtoTcpUdp returns the transport layer protocol name checked for
+// from a Payload/Cmp twin-expression, together with the remaining expressions;
+// otherwise, it returns nil.
+func PayloadL4ProtoTcpUdp(exprs nufftables.Expressions) (nufftables.Expressions, string) {
return nufftables.PrefixedOfTypeTransformed(exprs, isPayloadIPv4L4Proto, TcpUdp)
}
diff --git a/network/portfwd/nftget/l4proto_test.go b/network/portfwd/nftget/l4proto_test.go
index 868edc1..f9795cf 100644
--- a/network/portfwd/nftget/l4proto_test.go
+++ b/network/portfwd/nftget/l4proto_test.go
@@ -56,7 +56,7 @@ var _ = Describe("nftables L4 proto getter", func() {
if cmp != nil {
exprs = append(exprs, cmp)
}
- exprs, protoname := L4ProtoTcpUdp(exprs)
+ exprs, protoname := MetaL4ProtoTcpUdp(exprs)
if expectedName == "" {
Expect(exprs).To(BeNil())
} else {
diff --git a/webui/src/components/nifbadge/NifBadge.tsx b/webui/src/components/nifbadge/NifBadge.tsx
index 320a4ab..4851c02 100644
--- a/webui/src/components/nifbadge/NifBadge.tsx
+++ b/webui/src/components/nifbadge/NifBadge.tsx
@@ -9,7 +9,6 @@ import { Button, styled, SvgIconProps } from '@mui/material'
import HearingIcon from '@mui/icons-material/Hearing'
import { DormantIcon, DownIcon, LowerLayerDownIcon, UpIcon } from 'icons/operstates'
-import { BridgeIcon, BridgeInternalIcon, DummyIcon, HardwareNicIcon, HardwareNicPFIcon, HardwareNicVFIcon, MacvlanIcon, MacvlanMasterIcon, NicIcon, OverlayIcon, TapIcon, TunIcon, VethIcon } from 'icons/nifs'
import { AddressFamily, AddressFamilySet, GHOSTWIRE_LABEL_ROOT, NetworkInterface, nifId, orderAddresses, SRIOVRole } from 'models/gw'
import { OperationalState } from 'models/gw'
@@ -19,6 +18,7 @@ import { relationClassName } from 'utils/relclassname'
import { rgba } from 'utils/rgba'
import { TargetCapture } from 'components/targetcapture'
import { NifCheckbox } from 'components/nifcheckbox'
+import { NifIcon } from 'components/nificon'
// The outer span holding together an optional "hardware" NIC icon as well
@@ -174,33 +174,6 @@ const OperstateIndicator = styled('span')(({ theme }) => ({
[`&.${OperationalState.Dormant.toLowerCase()}`]: { color: theme.palette.operstate.dormant },
}))
-const nifSRIOVIcons = {
- [SRIOVRole.None]: HardwareNicIcon,
- [SRIOVRole.PF]: HardwareNicPFIcon,
- [SRIOVRole.VF]: HardwareNicVFIcon,
-}
-
-// Known network interface type icons, indexed by the kind property of network
-// interface objects (and directly taken from what Linux' RTNETLINK tells us).
-const nifTypeIcons: { [key: string]: (props: SvgIconProps) => JSX.Element } = {
- 'bridge': BridgeIcon,
- 'dummy': DummyIcon,
- 'macvlan': MacvlanIcon,
- 'tap': TapIcon,
- 'tun': TunIcon,
- 'veth': VethIcon,
- 'vxlan': OverlayIcon,
-}
-
-const nifIcon = (nif: NetworkInterface) => {
- if (GHOSTWIRE_LABEL_ROOT + 'bridge/internal' in nif.labels) {
- return BridgeInternalIcon
- }
- return (nif.tuntapDetails && nifTypeIcons[nif.tuntapDetails.mode]) ||
- (nif.macvlans && MacvlanMasterIcon) ||
- nifTypeIcons[nif.kind] || NicIcon
-}
-
const operStateIcons: { [key: string]: (props: SvgIconProps) => JSX.Element } = {
[OperationalState.Unknown]: UpIcon,
[OperationalState.Dormant]: DormantIcon,
@@ -343,11 +316,10 @@ export const NifBadge = ({
const alias = (nif.alias && nif.alias !== "") && <> (~{nif.alias})>
const vid = (nif.vlanDetails) && <> VID {nif.vlanDetails.vid}>
- const NifIcon = nifIcon(nif)
const OperstateIcon = operStateIcons[nif.operstate]
const content = <>
-
+
)}
{nif.isPhysical &&
-
+
}
{nif.isPromiscuous &&
diff --git a/webui/src/components/nifhwicon/NifHWIcon.tsx b/webui/src/components/nifhwicon/NifHWIcon.tsx
new file mode 100644
index 0000000..9a51961
--- /dev/null
+++ b/webui/src/components/nifhwicon/NifHWIcon.tsx
@@ -0,0 +1,30 @@
+// (c) Siemens AG 2024
+//
+// SPDX-License-Identifier: MIT
+
+import React from 'react'
+
+import { SvgIconProps } from '@mui/material'
+
+import HardwareNicIcon from 'icons/nifs/HardwareNic'
+import HardwareNicPFIcon from 'icons/nifs/HardwareNicPF'
+import HardwareNicVFIcon from 'icons/nifs/HardwareNicVF'
+import { NetworkInterface, SRIOVRole } from 'models/gw/nif'
+
+const nifSRIOVIcons = {
+ [SRIOVRole.None]: HardwareNicIcon,
+ [SRIOVRole.PF]: HardwareNicPFIcon,
+ [SRIOVRole.VF]: HardwareNicVFIcon,
+}
+
+export interface NifHWIconProps extends SvgIconProps {
+ /** network interface object describing a network interface in detail. */
+ nif: NetworkInterface
+}
+
+export const NifHWIcon = ({ nif, ...props }: NifHWIconProps) => {
+ const HWIcon = nifSRIOVIcons[nif.sriovrole || SRIOVRole.None]
+ return
+}
+
+export default NifHWIcon
diff --git a/webui/src/components/nifhwicon/index.ts b/webui/src/components/nifhwicon/index.ts
new file mode 100644
index 0000000..337b627
--- /dev/null
+++ b/webui/src/components/nifhwicon/index.ts
@@ -0,0 +1 @@
+export { NifHWIcon } from './NifHWIcon'
diff --git a/webui/src/components/nificon/NifIcon.tsx b/webui/src/components/nificon/NifIcon.tsx
new file mode 100644
index 0000000..62eed08
--- /dev/null
+++ b/webui/src/components/nificon/NifIcon.tsx
@@ -0,0 +1,55 @@
+// (c) Siemens AG 2024
+//
+// SPDX-License-Identifier: MIT
+
+import React from 'react'
+
+import { SvgIconProps } from '@mui/material'
+
+import { NetworkInterface } from 'models/gw/nif'
+import BridgeIcon from 'icons/nifs/Bridge'
+import DummyIcon from 'icons/nifs/Dummy'
+import MacvlanIcon from 'icons/nifs/Macvlan'
+import TapIcon from 'icons/nifs/Tap'
+import TunIcon from 'icons/nifs/Tun'
+import VethIcon from 'icons/nifs/Veth'
+import { BridgeInternalIcon, MacvlanMasterIcon, NicIcon, OverlayIcon } from 'icons/nifs'
+import { GHOSTWIRE_LABEL_ROOT } from 'models/gw/model'
+import { NifHWIcon } from 'components/nifhwicon'
+
+// Known network interface type icons, indexed by the kind property of network
+// interface objects (and directly taken from what Linux' RTNETLINK tells us).
+const nifTypeIcons: { [key: string]: (props: SvgIconProps) => JSX.Element } = {
+ 'bridge': BridgeIcon,
+ 'dummy': DummyIcon,
+ 'macvlan': MacvlanIcon,
+ 'tap': TapIcon,
+ 'tun': TunIcon,
+ 'veth': VethIcon,
+ 'vxlan': OverlayIcon,
+}
+
+export interface NifIconProps extends SvgIconProps {
+ /** network interface object describing a network interface in detail. */
+ nif: NetworkInterface
+ /** show HW NIC icon instead of generic icon if nic is "physical". */
+ considerPhysical?: boolean
+}
+
+export const NifIcon = ({ nif, considerPhysical, ...props }: NifIconProps) => {
+ if (!nif) {
+ return <>>
+ }
+ if (considerPhysical && nif.isPhysical) {
+ return
+ }
+ if (nif.labels && GHOSTWIRE_LABEL_ROOT + 'bridge/internal' in nif.labels) {
+ return
+ }
+ const Icon = (nif.tuntapDetails && nifTypeIcons[nif.tuntapDetails.mode]) ||
+ (nif.macvlans && MacvlanMasterIcon) ||
+ nifTypeIcons[nif.kind] || NicIcon
+ return
+}
+
+export default NifIcon
diff --git a/webui/src/components/nificon/index.ts b/webui/src/components/nificon/index.ts
new file mode 100644
index 0000000..fecf57c
--- /dev/null
+++ b/webui/src/components/nificon/index.ts
@@ -0,0 +1 @@
+export { NifIcon } from './NifIcon'
diff --git a/webui/src/components/nifinfomodal/NifInfoModal.tsx b/webui/src/components/nifinfomodal/NifInfoModal.tsx
index 6c2d76c..0650f55 100644
--- a/webui/src/components/nifinfomodal/NifInfoModal.tsx
+++ b/webui/src/components/nifinfomodal/NifInfoModal.tsx
@@ -7,15 +7,20 @@ import { styled } from '@mui/material'
import { NetworkInterface } from 'models/gw'
import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Snackbar, Tooltip } from '@mui/material'
import ClearIcon from '@mui/icons-material/Clear'
-import LanIcon from '@mui/icons-material/Lan'
import { ContentCopy } from '@mui/icons-material'
import CloseIcon from '@mui/icons-material/Close'
-const NifDialogTitle = styled(DialogTitle)(({theme}) => ({
+import { NifIcon } from 'components/nificon'
+
+const NifDialogTitle = styled(DialogTitle)(({ theme }) => ({
'& .close': {
position: 'relative',
right: theme.spacing(-1),
- top: theme.spacing(-0.25),
+ top: theme.spacing(-0.5),
+ },
+ '& .nificon.MuiSvgIcon-root': {
+ position: 'relative',
+ top: theme.spacing(0.5),
},
}))
@@ -116,7 +121,12 @@ export const NifInfoModalProvider = ({ children }: NifInfoModalProviderProps) =>
onClose={handleClose}
>
- Network Interface Information
+ Network Interface Information
{prop('interface name', nif.name)}
- {prop('type/kind', nif.kind || '(virtual) hardware')}
+ {prop('type/kind', nif.kind ? `virtual ${nif.kind}` : '(virtualized) hardware')}
{prop('driver', nif.driverinfo.driver)}
{prop('firmware version', nif.driverinfo.fwversion !== 'N/A' && nif.driverinfo.fwversion)}
{prop('ext ROM version', nif.driverinfo.eromversion)}