From f3d5a3107bf8ef6d7a862c744106b583350eec9b Mon Sep 17 00:00:00 2001 From: TheDiveO <6920158+thediveo@users.noreply.github.com> Date: Fri, 10 May 2024 16:18:38 +0200 Subject: [PATCH] Fix/docker container local portfwd discovery (#52) * refactor: use reusable nft information retrieval helpers Signed-off-by: thediveo * refactor: use dsl.TargetNAT instead of local implementation Signed-off-by: thediveo * refactor: fix package test fn names Signed-off-by: thediveo * test: add end-to-end docker port forwardings test host and container-local; refactor nft convenience expr getters, morbyd dep bump up Signed-off-by: thediveo * doc: coverage Signed-off-by: thediveo --------- Signed-off-by: thediveo --- README.md | 2 +- go.mod | 26 +++-- go.sum | 75 ++++++------ network/portfwd/docker/docker.go | 64 +++++++++++ network/portfwd/docker/docker_test.go | 121 ++++++++++++++++++++ network/portfwd/docker/package_test.go | 24 ++++ network/portfwd/kubeproxy/kubeproxy.go | 27 +---- network/portfwd/kubeproxy/kubeproxy_test.go | 60 ---------- network/portfwd/kubeproxy/package_test.go | 4 +- network/portfwd/nftget/doc.go | 5 + network/portfwd/nftget/ip.go | 44 +++++++ network/portfwd/nftget/ip_test.go | 83 ++++++++++++++ network/portfwd/nftget/l4proto.go | 45 ++++++++ network/portfwd/nftget/l4proto_test.go | 86 ++++++++++++++ network/portfwd/nftget/package_test.go | 24 ++++ network/portfwd/nftget/port.go | 19 +++ network/portfwd/nftget/port_test.go | 40 +++++++ 17 files changed, 616 insertions(+), 133 deletions(-) create mode 100644 network/portfwd/docker/docker_test.go create mode 100644 network/portfwd/docker/package_test.go create mode 100644 network/portfwd/nftget/doc.go create mode 100644 network/portfwd/nftget/ip.go create mode 100644 network/portfwd/nftget/ip_test.go create mode 100644 network/portfwd/nftget/l4proto.go create mode 100644 network/portfwd/nftget/l4proto_test.go create mode 100644 network/portfwd/nftget/package_test.go create mode 100644 network/portfwd/nftget/port.go create mode 100644 network/portfwd/nftget/port_test.go diff --git a/README.md b/README.md index 19467d8..56b09d8 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.4%25-yellow) +![Coverage](https://img.shields.io/badge/Coverage-77.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 4d76941..44c6a60 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,8 @@ 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.0 - github.com/docker/docker v26.0.2+incompatible + github.com/docker/docker v26.1.1+incompatible + github.com/docker/go-connections v0.5.0 github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 github.com/getkin/kin-openapi v0.124.0 github.com/google/nftables v0.2.1-0.20240422065334-aa8348f7904c @@ -15,8 +16,8 @@ require ( github.com/gorilla/websocket v1.5.1 github.com/jinzhu/copier v0.4.0 github.com/ohler55/ojg v1.21.5 - github.com/onsi/ginkgo/v2 v2.17.1 - github.com/onsi/gomega v1.33.0 + github.com/onsi/ginkgo/v2 v2.17.3 + github.com/onsi/gomega v1.33.1 github.com/ory/dockertest v3.3.5+incompatible github.com/ory/dockertest/v3 v3.10.0 github.com/siemens/ieddata v1.0.0 @@ -29,6 +30,7 @@ require ( github.com/thediveo/go-plugger/v3 v3.1.0 github.com/thediveo/ioctl v0.9.3 github.com/thediveo/lxkns v0.33.1 + github.com/thediveo/morbyd v0.11.1 github.com/thediveo/namspill v0.1.6 github.com/thediveo/netdb v1.1.2 github.com/thediveo/notwork v1.5.0 @@ -40,9 +42,9 @@ require ( github.com/thediveo/testbasher v1.0.8 github.com/thediveo/whalewatcher v0.11.3 github.com/vishvananda/netlink v1.2.1-beta.2.0.20240223175432-6ab7f5a3765c - golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f - golang.org/x/sys v0.19.0 - golang.org/x/text v0.14.0 + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 + golang.org/x/sys v0.20.0 + golang.org/x/text v0.15.0 sigs.k8s.io/kind v0.22.0 ) @@ -68,7 +70,6 @@ require ( github.com/containerd/typeurl/v2 v2.1.1 // indirect github.com/distribution/reference v0.5.0 // indirect github.com/docker/cli v25.0.4+incompatible // indirect - github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-units v0.5.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect @@ -79,12 +80,12 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.20.2 // indirect github.com/go-openapi/swag v0.22.8 // indirect - github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect + github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect @@ -102,14 +103,17 @@ require ( github.com/mdlayher/socket v0.5.0 // indirect github.com/miekg/dns v1.1.59 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/buildkit v0.13.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/mountinfo v0.7.1 // indirect github.com/moby/sys/sequential v0.5.0 // indirect github.com/moby/sys/signal v0.7.0 // indirect github.com/moby/sys/user v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/runc v1.1.11 // indirect @@ -131,9 +135,9 @@ require ( go.opentelemetry.io/otel/metric v1.22.0 // indirect go.opentelemetry.io/otel/trace v1.22.0 // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.24.0 // indirect + golang.org/x/net v0.25.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/tools v0.20.0 // indirect + golang.org/x/tools v0.21.0 // indirect google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/grpc v1.62.1 // indirect diff --git a/go.sum b/go.sum index 61ccb2e..55739e2 100644 --- a/go.sum +++ b/go.sum @@ -61,8 +61,8 @@ github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK github.com/distribution/reference v0.5.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 v26.0.2+incompatible h1:yGVmKUFGgcxA6PXWAokO0sQL22BrQ67cgVjko8tGdXE= -github.com/docker/docker v26.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v26.1.1+incompatible h1:oI+4kkAgIwwb54b9OC7Xc3hSgu1RlJA/Lln/DF72djQ= +github.com/docker/docker v26.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= @@ -102,8 +102,8 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -139,8 +139,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/nftables v0.2.1-0.20240422065334-aa8348f7904c h1:XJHEjE/d9/F9Sp6hvRCfh6Sl4WtCoKx7JJI2z1trH/Y= github.com/google/nftables v0.2.1-0.20240422065334-aa8348f7904c/go.mod h1:Fo/xFnOxWlRQtnHdNi46KbIjufTDzbKhtghpWrmsSUg= -github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= -github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 h1:SJ+NtwL6QaZ21U+IrK7d0gGgpjGGvd2kz+FzTHVzdqI= github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -198,8 +198,8 @@ github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moby/buildkit v0.12.5 h1:RNHH1l3HDhYyZafr5EgstEu8aGNCwyfvMtrQDtjH9T0= -github.com/moby/buildkit v0.12.5/go.mod h1:YGwjA2loqyiYfZeEo8FtI7z4x5XponAaIWsWcSjWwso= +github.com/moby/buildkit v0.13.2 h1:nXNszM4qD9E7QtG7bFWPnDI1teUQFQglBzon/IU3SzI= +github.com/moby/buildkit v0.13.2/go.mod h1:2cyVOv9NoHM7arphK9ZfHIWKn9YVZRFd1wXB8kKmEzY= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= @@ -230,13 +230,13 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= -github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= +github.com/onsi/ginkgo/v2 v2.17.3 h1:oJcvKpIb7/8uLpDDtnQuf18xVnwKp8DTD7DQ6gTd/MU= +github.com/onsi/ginkgo/v2 v2.17.3/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE= -github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -264,8 +264,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= -github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -291,7 +291,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -310,8 +309,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.33.1 h1:TJyTD0ZtHqBcr0SzM8OQ8mpXZzMzPEWUkLDl6Ylmjl4= github.com/thediveo/lxkns v0.33.1/go.mod h1:Mm31fCR45TSNACb0/H4aE92sP6VS5hURowtoew+Jn7M= -github.com/thediveo/morbyd v0.10.0 h1:WWcJUSEfuRJSh9xzXDmQdjE2susZygwnOf1dO3AbB1s= -github.com/thediveo/morbyd v0.10.0/go.mod h1:vMmifCTGup37clahfCDEOdzwCpC+Si5iIaRmsDhY4y0= +github.com/thediveo/morbyd v0.11.1 h1:YXkMUdYtNNsuVpZTk1wpRnU7GCrY+6qsVx0PwM6DhHY= +github.com/thediveo/morbyd v0.11.1/go.mod h1:y71pBj0NQECSdyavfL4ukYwNVxyM2wRlbhA41peuZmA= 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= @@ -356,26 +355,28 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfa go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= -go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= -go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= +golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -395,8 +396,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -425,16 +426,16 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -444,8 +445,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -500,8 +501,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/cri-api v0.29.3 h1:ppKSui+hhTJW774Mou6x+/ealmzt2jmTM0vsEQVWrjI= diff --git a/network/portfwd/docker/docker.go b/network/portfwd/docker/docker.go index 84afb46..f5fd94d 100644 --- a/network/portfwd/docker/docker.go +++ b/network/portfwd/docker/docker.go @@ -5,10 +5,14 @@ package docker import ( + "github.com/google/nftables/expr" + "github.com/google/nftables/xt" "github.com/siemens/ghostwire/v2/network/portfwd" + "github.com/siemens/ghostwire/v2/network/portfwd/nftget" "github.com/thediveo/go-plugger/v3" "github.com/thediveo/lxkns/log" "github.com/thediveo/nufftables" + "github.com/thediveo/nufftables/dsl" "github.com/thediveo/nufftables/portfinder" ) @@ -30,6 +34,15 @@ func PortForwardings(tables nufftables.TableMap, family nufftables.TableFamily) if nattable == nil { return nil } + forwardedPorts := forwardedPortsMk1(nattable) + forwardedPorts = append(forwardedPorts, forwardedPortsMk2(nattable)...) + return forwardedPorts +} + +// forwardedPortsMk1 discovers host and container-local port forwarding rules +// (especially for Docker's embedded DNS resolver) as created by older Docker +// versions and/or an older iptables compatibility layer. +func forwardedPortsMk1(nattable *nufftables.Table) []*portfinder.ForwardedPortRange { forwardedPorts := []*portfinder.ForwardedPortRange{} for _, chain := range nattable.ChainsByName { for _, rule := range chain.Rules { @@ -43,3 +56,54 @@ func PortForwardings(tables nufftables.TableMap, family nufftables.TableFamily) } return forwardedPorts } + +// forwardedPortsInChainMk2 discovers container-local port forwarding rules from +// the given chain. When given a nil chain, it simply returns a nil slice. +func forwardedPortsInChainMk2(chain *nufftables.Chain) []*portfinder.ForwardedPortRange { + if chain == nil { + return nil + } + family := chain.Table.Family + forwardedPorts := []*portfinder.ForwardedPortRange{} + for _, rule := range chain.Rules { + exprs, proto := nftget.L4ProtoTcpUdp(rule.Exprs) + exprs, origIP := nftget.OptionalIPv46(exprs, family) + exprs, port := nufftables.OfTypeTransformed(exprs, nftget.Port) + exprs, dnat := dsl.TargetDNAT(exprs) + if exprs == nil || dnat.Flags&dnatWithIPsAndPorts != dnatWithIPsAndPorts || port == 0 { + continue + } + fp := &portfinder.ForwardedPortRange{ + Protocol: proto, + IP: origIP, + PortMin: port, + PortMax: port, + ForwardIP: dnat.MinIP, + ForwardPortMin: dnat.MinPort, + } + log.Debugf("discovered %s", fp) + forwardedPorts = append(forwardedPorts, fp) + } + return forwardedPorts +} + +// dnatWithIPsAndPorts are the flags that need to be set in order for the +// xt.NatRange(2) data structures to contain an IP range as well as a transport +// layer port range. +const dnatWithIPsAndPorts = uint(xt.NatRangeMapIPs | xt.NatRangeProtoSpecified) + +// forwardedPortsMk2 discovers container-local port forwarding rules (such as +// for Docker's embedded DNS resolver) as created by newer Docker versions +// (25+?) and/or "newer" iptables compatibility layer. +func forwardedPortsMk2(nattable *nufftables.Table) []*portfinder.ForwardedPortRange { + forwardedPorts := forwardedPortsInChainMk2(nattable.ChainsByName["DOCKER"]) + forwardedPorts = append(forwardedPorts, forwardedPortsInChainMk2(nattable.ChainsByName["DOCKER_OUTPUT"])...) + return forwardedPorts +} + +// isL4Proto returns true if the given Meta expression accesses the L4PROTO key. +// See also "Matching Packet metainformation" from the nftables wiki: +// https://wiki.nftables.org/wiki-nftables/index.php/Matching_packet_metainformation +func isL4Proto(meta *expr.Meta) bool { + return meta.Key == expr.MetaKeyL4PROTO +} diff --git a/network/portfwd/docker/docker_test.go b/network/portfwd/docker/docker_test.go new file mode 100644 index 0000000..72f16e0 --- /dev/null +++ b/network/portfwd/docker/docker_test.go @@ -0,0 +1,121 @@ +// (c) Siemens AG 2024 +// +// SPDX-License-Identifier: MIT + +package docker + +import ( + "context" + "fmt" + "net" + "os" + "strconv" + "strings" + + "github.com/google/nftables" + "github.com/thediveo/morbyd" + "github.com/thediveo/morbyd/run" + "github.com/thediveo/morbyd/session" + "github.com/thediveo/notwork/netns" + "github.com/thediveo/nufftables" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/thediveo/success" +) + +var _ = Describe("Docker port forwarding", Ordered, func() { + + var cntrPID int + var hostPort uint16 + var cntrIP net.IP // 127.0.0.1 -> ?.?.?.? + + BeforeAll(func(ctx context.Context) { + if os.Getuid() != 0 { + Skip("needs root") + } + + sess := Successful(morbyd.NewSession(ctx, + session.WithAutoCleaning("test=ghostwire.network.portfwd.docker"))) + DeferCleanup(func(ctx context.Context) { + sess.Close(ctx) + }) + By("creating a temporary Docker custom test network") + netw := Successful(sess.CreateNetwork(ctx, + "test-portfwd-docker")) + By("spinning up a temporary test container, exposing a useless port on a random host port") + cntr := Successful(sess.Run(ctx, + "busybox", + run.WithNetwork(netw.ID), + run.WithPublishedPort("127.0.0.1:1234/tcp"), + run.WithCommand("/bin/sh", "-c", "while true; do sleep 1; done"), + )) + cntrPID = Successful(cntr.PID(ctx)) + By("picking up the random exposed host port number") + svcAddr := cntr.PublishedPort("1234/tcp").First().String() // there's only one + Expect(svcAddr).To(MatchRegexp(`127\.0\.0\.1:\d+`)) + hostPort = uint16(Successful(strconv.ParseUint(strings.Split(svcAddr, ":")[1], 10, 16))) + cntrIP = cntr.IP(ctx).To4() + Expect(hostPort).To(BeNumerically(">", uint16(32767))) + }) + + It("discovers host's port forwarding to container", func() { + conn := Successful(nftables.New(nftables.AsLasting())) + DeferCleanup(func() { + _ = conn.CloseLasting() + }) + tables := Successful(nufftables.GetAllTables(conn)) + nattable := tables.Table("nat", nufftables.TableFamilyIPv4) + Expect(nattable).NotTo(BeNil()) + Expect(nattable.ChainsByName).NotTo(BeEmpty()) + forwardedPorts := forwardedPortsMk1(nattable) + forwardedPorts = append(forwardedPorts, forwardedPortsMk2(nattable)...) + Expect(forwardedPorts).To(ContainElement(And( + HaveField("Protocol", "tcp"), + HaveField("IP", net.ParseIP("127.0.0.1").To4()), + HaveField("PortMin", hostPort), + HaveField("PortMax", hostPort), + HaveField("ForwardIP", cntrIP), + HaveField("ForwardPortMin", uint16(1234)), + ))) + }) + + It("discovers container-local Docker embedded DNS port forwarding", func() { + cntrnetnsf := Successful(os.Open(fmt.Sprintf("/proc/%d/ns/net", cntrPID))) + DeferCleanup(func() { cntrnetnsf.Close() }) + var conn *nftables.Conn + var err error + netns.Execute(int(cntrnetnsf.Fd()), func() { + conn, err = nftables.New(nftables.AsLasting()) + }) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { + _ = conn.CloseLasting() + }) + tables := Successful(nufftables.GetAllTables(conn)) + nattable := tables.Table("nat", nufftables.TableFamilyIPv4) + Expect(nattable).NotTo(BeNil()) + Expect(nattable.ChainsByName).NotTo(BeEmpty()) + forwardedPorts := forwardedPortsMk1(nattable) + forwardedPorts = append(forwardedPorts, forwardedPortsMk2(nattable)...) + Expect(forwardedPorts).To(ContainElements( + And( + HaveField("Protocol", "tcp"), + HaveField("IP", net.ParseIP("127.0.0.11").To4()), + HaveField("PortMin", uint16(53)), + HaveField("PortMax", uint16(53)), + HaveField("ForwardIP", net.ParseIP("127.0.0.11").To4()), + HaveField("ForwardPortMin", Not(BeZero())), + ), + And( + HaveField("Protocol", "udp"), + HaveField("IP", net.ParseIP("127.0.0.11").To4()), + HaveField("PortMin", uint16(53)), + HaveField("PortMax", uint16(53)), + HaveField("ForwardIP", net.ParseIP("127.0.0.11").To4()), + HaveField("ForwardPortMin", Not(BeZero())), + ), + )) + }) + +}) diff --git a/network/portfwd/docker/package_test.go b/network/portfwd/docker/package_test.go new file mode 100644 index 0000000..d36622c --- /dev/null +++ b/network/portfwd/docker/package_test.go @@ -0,0 +1,24 @@ +// (c) Siemens AG 2024 +// +// SPDX-License-Identifier: MIT + +package docker + +import ( + "runtime" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func init() { + // avoid M0 ending up wedged as it was used during a throw-away namespace + // switch, but as M0 is special it cannot be killed. + runtime.LockOSThread() +} + +func TestDockerForwarding(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ghostwire/network/portfwd/docker package") +} diff --git a/network/portfwd/kubeproxy/kubeproxy.go b/network/portfwd/kubeproxy/kubeproxy.go index 55094c8..5238052 100644 --- a/network/portfwd/kubeproxy/kubeproxy.go +++ b/network/portfwd/kubeproxy/kubeproxy.go @@ -11,8 +11,10 @@ import ( "github.com/google/nftables/expr" "github.com/google/nftables/xt" "github.com/siemens/ghostwire/v2/network/portfwd" + "github.com/siemens/ghostwire/v2/network/portfwd/nftget" "github.com/thediveo/go-plugger/v3" "github.com/thediveo/nufftables" + "github.com/thediveo/nufftables/dsl" "github.com/thediveo/nufftables/portfinder" "golang.org/x/sys/unix" ) @@ -59,7 +61,7 @@ func PortForwardings(tables nufftables.TableMap, family nufftables.TableFamily) continue } for _, rule := range sc.Rules { - _, dnat := nufftables.OfTypeTransformed(rule.Exprs, getDNAT) + _, dnat := dsl.TargetDNAT(rule.Exprs) if dnat == nil { continue } @@ -78,15 +80,6 @@ func PortForwardings(tables nufftables.TableMap, family nufftables.TableFamily) return forwardedPorts } -// getDNAT returns the DNAT target expression, otherwise false. -func getDNAT(target *expr.Target) (*xt.NatRange2, bool) { - if target.Name != "DNAT" { - return nil, false - } - nr, ok := target.Info.(*xt.NatRange2) - return nr, ok -} - // serviceProviderChains returns a list of service separation chain names, given // a specific service chain. func serviceProviderChains(chain *nufftables.Chain) (chains []string) { @@ -124,9 +117,9 @@ func virtualServiceDetails( // if there is any problem, then we will end up with nil remaining // expressions as our warning signal. exprs, protocol = nufftables.OfTypeTransformed(exprs, getTcpUdp) - exprs, ip = nufftables.OfTypeTransformed(exprs, getIPv46) + exprs, ip = nufftables.OfTypeTransformed(exprs, nftget.IPv46) exprs, comment = nufftables.OfTypeTransformed(exprs, getComment) - exprs, port = nufftables.OfTypeTransformed(exprs, getPort) + exprs, port = nufftables.OfTypeTransformed(exprs, nftget.Port) exprs, chain = nufftables.OfTypeTransformed(exprs, getJumpVerdictServiceChain) if exprs == nil { return nil, "", 0, "", "" @@ -149,16 +142,6 @@ func getTcpUdp(cmp *expr.Cmp) (string, bool) { return "", false } -// getIPv46 returns the IPv4 or IPv6 address enclosed in a Cmp expression, -// otherwise false. -func getIPv46(cmp *expr.Cmp) (net.IP, bool) { - switch len(cmp.Data) { - case 4, 16: - return net.IP(cmp.Data), true - } - return nil, false -} - // getComment returns the comment enclosed in a “comment” Match expression, or // otherwise false if the passed Match expression isn't a comment. Ah, turtles // all the way down. diff --git a/network/portfwd/kubeproxy/kubeproxy_test.go b/network/portfwd/kubeproxy/kubeproxy_test.go index c7f989a..176a695 100644 --- a/network/portfwd/kubeproxy/kubeproxy_test.go +++ b/network/portfwd/kubeproxy/kubeproxy_test.go @@ -196,26 +196,6 @@ var _ = Describe("kube-proxy port forwarding", func() { }) - It("matches DNAT target expressions", func() { - dnat, ok := getDNAT(&expr.Target{Name: "hellorld"}) - Expect(ok).To(BeFalse()) - Expect(dnat).To(BeNil()) - - dnat, ok = getDNAT(&expr.Target{Name: "DNAT"}) - Expect(ok).To(BeFalse()) - Expect(dnat).To(BeNil()) - - dnat, ok = getDNAT(&expr.Target{ - Name: "DNAT", - Info: &xt.NatRange2{ - BasePort: 42, - }, - }) - Expect(ok).To(BeTrue()) - Expect(dnat).NotTo(BeNil()) - Expect(dnat.BasePort).To(Equal(uint16(42))) - }) - Context("service provider chains", func() { It("returns nothing for non-existing chain", func() { @@ -290,46 +270,6 @@ var _ = Describe("kube-proxy port forwarding", func() { }) - Context("IP addresses", func() { - - It("ignores Cmp expressions with other data", func() { - ip, ok := getIPv46(&expr.Cmp{}) - Expect(ok).To(BeFalse()) - Expect(ip).To(BeZero()) - - ip, ok = getIPv46(&expr.Cmp{Data: []byte{1, 2, 3}}) - Expect(ok).To(BeFalse()) - Expect(ip).To(BeZero()) - }) - - It("returns port", func() { - ip, ok := getIPv46(&expr.Cmp{Data: []byte(net.ParseIP("fe80::dead:beef"))}) - Expect(ok).To(BeTrue()) - Expect(ip).To(Equal(net.ParseIP("fe80::dead:beef"))) - }) - - }) - - Context("ports", func() { - - It("ignores Cmp expressions with other data", func() { - port, ok := getPort(&expr.Cmp{}) - Expect(ok).To(BeFalse()) - Expect(port).To(BeZero()) - - port, ok = getPort(&expr.Cmp{Data: []byte{1, 2, 3}}) - Expect(ok).To(BeFalse()) - Expect(port).To(BeZero()) - }) - - It("returns port", func() { - port, ok := getPort(&expr.Cmp{Data: []byte{1, 2}}) - Expect(ok).To(BeTrue()) - Expect(port).To(Equal(uint16(0x0102))) - }) - - }) - Context("service jump verdicts", func() { It("ignores non-service jump targets other verdicts", func() { diff --git a/network/portfwd/kubeproxy/package_test.go b/network/portfwd/kubeproxy/package_test.go index d721659..867c2c3 100644 --- a/network/portfwd/kubeproxy/package_test.go +++ b/network/portfwd/kubeproxy/package_test.go @@ -18,7 +18,7 @@ func init() { runtime.LockOSThread() } -func TestGostwireNetwork(t *testing.T) { +func TestKubeproxyForwarding(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "ghostwire/network/kubeproxy package") + RunSpecs(t, "ghostwire/network/portfwd/kubeproxy package") } diff --git a/network/portfwd/nftget/doc.go b/network/portfwd/nftget/doc.go new file mode 100644 index 0000000..6513026 --- /dev/null +++ b/network/portfwd/nftget/doc.go @@ -0,0 +1,5 @@ +/* +Package nftget help with conveniently retrieving information from nftable +expressions. +*/ +package nftget diff --git a/network/portfwd/nftget/ip.go b/network/portfwd/nftget/ip.go new file mode 100644 index 0000000..f2196d5 --- /dev/null +++ b/network/portfwd/nftget/ip.go @@ -0,0 +1,44 @@ +// (c) Siemens AG 2024 +// +// SPDX-License-Identifier: MIT + +package nftget + +import ( + "net" + + "github.com/google/nftables" + "github.com/google/nftables/expr" + "github.com/thediveo/nufftables" +) + +var iPv4Unspecified = net.IP{0, 0, 0, 0} + +// OptionalIPv46 returns the IPv4 or IPv6 address enclosed in a Cmp expression, +// otherwise the unspecified IP address of the particular IP family. +func OptionalIPv46(exprs nufftables.Expressions, family nftables.TableFamily) (nufftables.Expressions, net.IP) { + remexpr, ip := nufftables.OfTypeTransformed(exprs, IPv46) + if remexpr == nil { + switch family { + case nftables.TableFamilyIPv4: + return exprs, iPv4Unspecified + case nftables.TableFamilyIPv6: + return exprs, net.IPv6unspecified + } + return exprs, nil + } + return remexpr, ip +} + +// IPv46 returns the IPv4 or IPv6 address enclosed in a Cmp expression, +// otherwise false. +func IPv46(cmp *expr.Cmp) (net.IP, bool) { + if cmp.Op != expr.CmpOpEq { + return nil, false + } + switch len(cmp.Data) { + case 4, 16: + return net.IP(cmp.Data), true + } + return nil, false +} diff --git a/network/portfwd/nftget/ip_test.go b/network/portfwd/nftget/ip_test.go new file mode 100644 index 0000000..500f535 --- /dev/null +++ b/network/portfwd/nftget/ip_test.go @@ -0,0 +1,83 @@ +// (c) Siemens AG 2024 +// +// SPDX-License-Identifier: MIT + +package nftget + +import ( + "net" + + "github.com/google/nftables" + "github.com/google/nftables/expr" + "github.com/thediveo/nufftables" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("nftables L3 IP address getter", func() { + + DescribeTable("matches only IPv4 or IPv6 Cmp", + func(expr expr.Any, expectedIP net.IP) { + exprs, ip := nufftables.OfTypeTransformed( + nufftables.Expressions{ + expr, + }, + IPv46, + ) + if expectedIP == nil { + Expect(exprs).To(BeNil()) + } else { + Expect(exprs).NotTo(BeNil()) + } + Expect(ip).To(Equal(expectedIP)) + }, + Entry(nil, &expr.Cmp{Op: expr.CmpOpGt}, nil), + Entry(nil, &expr.Cmp{Op: expr.CmpOpEq}, nil), + Entry(nil, &expr.Cmp{ + Op: expr.CmpOpEq, + Data: []byte{192, 168, 0, 1}, + }, net.ParseIP("192.168.0.1").To4()), + Entry(nil, &expr.Cmp{ + Op: expr.CmpOpEq, + Data: []byte{ + 0xfe, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0xde, 0xad, 0xbe, 0xef, + }, + }, net.ParseIP("fe80::dead:beef")), + ) + + DescribeTable("optional matches IPv4 or IPv6, defaulting to unspecified IP", + func(expr expr.Any, family nftables.TableFamily, expectedIP net.IP) { + exprs, ip := OptionalIPv46( + nufftables.Expressions{ + expr, + }, + family, + ) + if expectedIP == nil { + Expect(exprs).To(BeNil()) + } else { + Expect(exprs).NotTo(BeNil()) + } + Expect(ip).To(Equal(expectedIP)) + }, + Entry(nil, &expr.Cmp{Op: expr.CmpOpGt}, nftables.TableFamilyIPv4, net.IP{0, 0, 0, 0}), + Entry(nil, &expr.Cmp{Op: expr.CmpOpGt}, nftables.TableFamilyIPv6, net.IPv6unspecified), + Entry(nil, &expr.Cmp{ + Op: expr.CmpOpEq, + Data: []byte{192, 168, 0, 1}, + }, nftables.TableFamilyIPv4, net.ParseIP("192.168.0.1").To4()), + Entry(nil, &expr.Cmp{ + Op: expr.CmpOpEq, + Data: []byte{ + 0xfe, 0x80, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0xde, 0xad, 0xbe, 0xef, + }, + }, nftables.TableFamilyIPv4, net.ParseIP("fe80::dead:beef")), + ) +}) diff --git a/network/portfwd/nftget/l4proto.go b/network/portfwd/nftget/l4proto.go new file mode 100644 index 0000000..56c5b10 --- /dev/null +++ b/network/portfwd/nftget/l4proto.go @@ -0,0 +1,45 @@ +// (c) Siemens AG 2024 +// +// SPDX-License-Identifier: MIT + +package nftget + +import ( + "github.com/google/nftables/expr" + "github.com/thediveo/nufftables" + "golang.org/x/sys/unix" +) + +// L4ProtoTcpUdp returns the transport layer protocol name checked for from a +// Meta/Cmp combo together with the remaining expressions; otherwise, it returns +// nil. +func L4ProtoTcpUdp(exprs nufftables.Expressions) (nufftables.Expressions, string) { + exprs, _ = nufftables.OfTypeFunc(exprs, isL4Proto) + if exprs == nil { + return nil, "" + } + exprs, proto := nufftables.OfTypeTransformed(exprs, TcpUdp) + return exprs, proto +} + +// isL4Proto returns true if the given Meta expression accesses the L4PROTO key. +// See also "Matching Packet metainformation" from the nftables wiki: +// https://wiki.nftables.org/wiki-nftables/index.php/Matching_packet_metainformation +func isL4Proto(meta *expr.Meta) bool { + return meta.Key == expr.MetaKeyL4PROTO +} + +// TcpUdp returns the transport protocol name enclosed in a Cmp expression +// testing the L4 protocol for TCP and UDP, otherwise false. +func TcpUdp(cmp *expr.Cmp) (string, bool) { + if cmp.Op != expr.CmpOpEq || len(cmp.Data) != 1 { + return "", false + } + switch cmp.Data[0] { + case unix.IPPROTO_TCP: + return "tcp", true + case unix.IPPROTO_UDP: + return "udp", true + } + return "", false +} diff --git a/network/portfwd/nftget/l4proto_test.go b/network/portfwd/nftget/l4proto_test.go new file mode 100644 index 0000000..868edc1 --- /dev/null +++ b/network/portfwd/nftget/l4proto_test.go @@ -0,0 +1,86 @@ +// (c) Siemens AG 2024 +// +// SPDX-License-Identifier: MIT + +package nftget + +import ( + "github.com/google/nftables/expr" + "github.com/thediveo/nufftables" + "golang.org/x/sys/unix" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("nftables L4 proto getter", func() { + + DescribeTable("matches only TCP or UDP Cmp", + func(expr expr.Any, expectedName string) { + exprs, protoname := nufftables.OfTypeTransformed( + nufftables.Expressions{ + expr, + }, + TcpUdp, + ) + if expectedName == "" { + Expect(exprs).To(BeNil()) + } else { + Expect(exprs).NotTo(BeNil()) + } + Expect(protoname).To(Equal(expectedName)) + }, + Entry(nil, &expr.Meta{Key: expr.MetaKeyCGROUP}, ""), + Entry(nil, &expr.Cmp{Op: expr.CmpOpGt}, ""), + Entry(nil, &expr.Cmp{Op: expr.CmpOpEq}, ""), + Entry(nil, &expr.Cmp{ + Op: expr.CmpOpEq, + Data: []byte{unix.IPPROTO_ETHERNET}, + }, ""), + Entry(nil, &expr.Cmp{ + Op: expr.CmpOpEq, + Data: []byte{unix.IPPROTO_TCP}, + }, "tcp"), + Entry(nil, &expr.Cmp{ + Op: expr.CmpOpEq, + Data: []byte{unix.IPPROTO_UDP}, + }, "udp"), + ) + + DescribeTable("matches only TCP or UDP Meta+Cmp combo", + func(meta *expr.Meta, cmp *expr.Cmp, expectedName string) { + exprs := nufftables.Expressions{} + if meta != nil { + exprs = append(exprs, meta) + } + if cmp != nil { + exprs = append(exprs, cmp) + } + exprs, protoname := L4ProtoTcpUdp(exprs) + if expectedName == "" { + Expect(exprs).To(BeNil()) + } else { + Expect(exprs).NotTo(BeNil()) + } + Expect(protoname).To(Equal(expectedName)) + }, + Entry(nil, nil, nil, ""), + Entry(nil, &expr.Meta{}, nil, ""), + Entry(nil, &expr.Meta{ + Key: expr.MetaKeyL4PROTO, + }, nil, ""), + Entry(nil, &expr.Meta{ + Key: expr.MetaKeyL4PROTO, + }, &expr.Cmp{ + Op: expr.CmpOpEq, + Data: []byte{unix.IPPROTO_ETHERNET}, + }, ""), + Entry(nil, &expr.Meta{ + Key: expr.MetaKeyL4PROTO, + }, &expr.Cmp{ + Op: expr.CmpOpEq, + Data: []byte{unix.IPPROTO_TCP}, + }, "tcp"), + ) + +}) diff --git a/network/portfwd/nftget/package_test.go b/network/portfwd/nftget/package_test.go new file mode 100644 index 0000000..ac52253 --- /dev/null +++ b/network/portfwd/nftget/package_test.go @@ -0,0 +1,24 @@ +// (c) Siemens AG 2024 +// +// SPDX-License-Identifier: MIT + +package nftget + +import ( + "runtime" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func init() { + // avoid M0 ending up wedged as it was used during a throw-away namespace + // switch, but as M0 is special it cannot be killed. + runtime.LockOSThread() +} + +func TestNftget(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ghostwire/network/portfwd/nftget package") +} diff --git a/network/portfwd/nftget/port.go b/network/portfwd/nftget/port.go new file mode 100644 index 0000000..be53b84 --- /dev/null +++ b/network/portfwd/nftget/port.go @@ -0,0 +1,19 @@ +// (c) Siemens AG 2024 +// +// SPDX-License-Identifier: MIT + +package nftget + +import ( + "github.com/google/nftables/expr" +) + +// Port returns the (transport) port number from a Cmp expression; otherwise, +// returns false. +func Port(cmp *expr.Cmp) (uint16, bool) { + if cmp.Op != expr.CmpOpEq || len(cmp.Data) != 2 { + return 0, false + } + // port number is always in network order, that is, big endian. + return uint16(cmp.Data[0])<<8 + uint16(cmp.Data[1]), true +} diff --git a/network/portfwd/nftget/port_test.go b/network/portfwd/nftget/port_test.go new file mode 100644 index 0000000..49bbeb7 --- /dev/null +++ b/network/portfwd/nftget/port_test.go @@ -0,0 +1,40 @@ +// (c) Siemens AG 2024 +// +// SPDX-License-Identifier: MIT + +package nftget + +import ( + "github.com/google/nftables/expr" + "github.com/thediveo/nufftables" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("nftables L4 port getter", func() { + + DescribeTable("matches only L4 Port Cmp", + func(expr expr.Any, expectedPort uint16, expectedOk bool) { + exprs, port := nufftables.OfTypeTransformed( + nufftables.Expressions{ + expr, + }, + Port, + ) + if expectedOk { + Expect(exprs).NotTo(BeNil()) + } else { + Expect(exprs).To(BeNil()) + } + Expect(port).To(Equal(expectedPort)) + }, + Entry(nil, &expr.Cmp{Op: expr.CmpOpGt}, uint16(0), false), + Entry(nil, &expr.Cmp{Op: expr.CmpOpEq}, uint16(0), false), + Entry(nil, &expr.Cmp{ + Op: expr.CmpOpEq, + Data: []byte{0x12, 0x34}, + }, uint16(0x1234), true), + ) + +})