From bdb36063aaac94dc4ada6e5c6652e3cb74147834 Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Sat, 15 Nov 2025 21:43:00 +0100 Subject: [PATCH 1/8] Implement network namespace architecture with integrated DHCP Add complete network isolation using Linux namespaces with integrated DHCP server to eliminate host system contamination and enable clean VM management operations. Key Changes * Network Namespace Management: Idempotent namespace creation with persistent VM isolation * Integrated DHCP Server: CoreDHCP integration within namespace prevents system conflicts * Platform Separation: Linux namespace implementation vs Darwin direct execution * Recursive Process Execution: Ensures all goroutines run within target namespace * Dynamic Interface Adaptation: Automatic firewall reconfiguration on interface changes Benefits * Zero host networking contamination during VM operations * Crash-resilient design - VMs survive management process restarts * No external DHCP configuration dependencies * Clean operational boundaries between system and VM networking Signed-off-by: Nikita Vakula --- go.mod | 40 ++++- go.sum | 116 ++++++++++++-- network-utils | 2 +- src/pkg/protos/process.go | 2 +- src/pkg/utils/network/networkmanager_linux.go | 90 +---------- src/protos/settings/v1/settings.proto | 17 +- src/qcontrollerd/cmd/config_linux.go | 69 ++++++++ src/qcontrollerd/cmd/qemu.go | 125 --------------- src/qcontrollerd/cmd/qemu_common.go | 86 ++++++++++ src/qcontrollerd/cmd/qemu_darwin.go | 63 ++++++++ src/qcontrollerd/cmd/qemu_linux.go | 149 ++++++++++++++++++ start.sh | 37 ++++- 12 files changed, 549 insertions(+), 247 deletions(-) create mode 100644 src/qcontrollerd/cmd/config_linux.go delete mode 100644 src/qcontrollerd/cmd/qemu.go create mode 100644 src/qcontrollerd/cmd/qemu_common.go create mode 100644 src/qcontrollerd/cmd/qemu_darwin.go create mode 100644 src/qcontrollerd/cmd/qemu_linux.go diff --git a/go.mod b/go.mod index a6145f8..c5143de 100644 --- a/go.mod +++ b/go.mod @@ -21,36 +21,62 @@ require ( require ( github.com/KyleBanks/depth v1.2.1 // indirect + github.com/bits-and-blooms/bitset v1.24.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb // indirect + github.com/coredhcp/coredhcp v0.0.0-20250927164030-d2ed887fca9b // indirect github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/spec v0.20.6 // indirect github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/gopacket v1.1.19 // indirect github.com/google/nftables v0.3.0 // indirect + github.com/insomniacslk/dhcp v0.0.0-20241203100832-a481575ed0ef // indirect github.com/josharian/intern v1.0.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect github.com/mdlayher/socket v0.5.0 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/viper v1.20.1 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/swaggo/swag v1.8.1 // indirect + github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect github.com/vishvananda/netns v0.0.5 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.26.0 // indirect - golang.org/x/tools v0.33.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.42.0 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/term v0.35.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/tools v0.36.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) @@ -58,9 +84,9 @@ require ( github.com/dgraph-io/badger/v4 v4.8.0 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/q-controller/qapi-client v0.0.0-00010101000000-000000000000 - github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/pflag v1.0.7 // indirect github.com/swaggo/http-swagger/v2 v2.0.2 - github.com/vishvananda/netlink v1.3.1 - golang.org/x/sync v0.15.0 + github.com/vishvananda/netlink v1.3.1 // indirect + golang.org/x/sync v0.17.0 google.golang.org/protobuf v1.36.6 ) diff --git a/go.sum b/go.sum index 9237109..bf2c4c0 100644 --- a/go.sum +++ b/go.sum @@ -2,13 +2,20 @@ github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs= github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= +github.com/bits-and-blooms/bitset v1.24.0 h1:H4x4TuulnokZKvHLfzVRTHJfFfnHEeSYJizujEZvmAM= +github.com/bits-and-blooms/bitset v1.24.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb h1:aZTKxMminKeQWHtzJBbV8TttfTxzdJ+7iEJFE6FmUzg= +github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb/go.mod h1:xzXc1S/L+64uglB3pw54o8kqyM6KFYpTeC9Q6+qZIu8= +github.com/coredhcp/coredhcp v0.0.0-20250927164030-d2ed887fca9b h1:L9ffVtIOGfIuduN7ZY7E0Z8rI2vbcwizZytbNrm5mHs= +github.com/coredhcp/coredhcp v0.0.0-20250927164030-d2ed887fca9b/go.mod h1:+FFQuUBeTgdrHkY6EyWdG3jm3jzt2dGrjl8amnmyoDw= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs= github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w= github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= @@ -17,6 +24,10 @@ github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa5 github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -32,12 +43,16 @@ github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg= github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -46,8 +61,12 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0 h1:+epNPbD5EqgpEMm5wrl4Hqts3jZ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/insomniacslk/dhcp v0.0.0-20241203100832-a481575ed0ef h1:NzQKDfd5ZOPnuZYf9MnRee8x2qecsVqzsnaLjEZiBko= +github.com/insomniacslk/dhcp v0.0.0-20241203100832-a481575ed0ef/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -61,39 +80,83 @@ github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg= github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o= +github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= +github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE= github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= +github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM= +github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= @@ -106,20 +169,41 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= -golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= -golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +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.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/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.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY= google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc= google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= @@ -133,6 +217,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/network-utils b/network-utils index 3a5ed00..76d75e9 160000 --- a/network-utils +++ b/network-utils @@ -1 +1 @@ -Subproject commit 3a5ed000c669b387afaa683347c9a3b09ef6c924 +Subproject commit 76d75e9d6c25489e24fa98cff5c10c7b73526c6c diff --git a/src/pkg/protos/process.go b/src/pkg/protos/process.go index cbc3e01..a05321d 100644 --- a/src/pkg/protos/process.go +++ b/src/pkg/protos/process.go @@ -237,7 +237,7 @@ func NewQemuService(monitor *process.InstanceMonitor, config *settingsv1.QemuCon } if linuxSettings := config.GetLinuxSettings(); linuxSettings != nil { - nm, nmErr := network.NewNetworkManager(linuxSettings.Bridge.Name, linuxSettings.Bridge.Subnet) + nm, nmErr := network.NewNetworkManager(linuxSettings.Network.Name, linuxSettings.Network.BridgeIp) if nmErr != nil { return nil, nmErr } diff --git a/src/pkg/utils/network/networkmanager_linux.go b/src/pkg/utils/network/networkmanager_linux.go index ad55db4..ae9624c 100644 --- a/src/pkg/utils/network/networkmanager_linux.go +++ b/src/pkg/utils/network/networkmanager_linux.go @@ -1,19 +1,11 @@ package network import ( - "errors" - "log/slog" - "net" "sync" - "github.com/q-controller/network-utils/src/utils/network/firewall" "github.com/q-controller/network-utils/src/utils/network/ifc" - "github.com/vishvananda/netlink" - "github.com/vishvananda/netlink/nl" ) -var ErrNetworkDisconnected = errors.New("network disconnected") - type linuxNetworkManager struct { bridgeName string mu sync.RWMutex @@ -25,25 +17,6 @@ func (m *linuxNetworkManager) Close() { m.done <- struct{}{} } -// getDefaultInterface finds the default network interface -func getDefaultInterface() (string, error) { - routes, err := netlink.RouteList(nil, nl.FAMILY_V4) - if err != nil { - return "", err - } - - for _, route := range routes { - if route.Dst.IP.IsUnspecified() && route.Dst.Mask.String() == net.CIDRMask(0, 32).String() { - link, err := netlink.LinkByIndex(route.LinkIndex) - if err != nil { - continue - } - return link.Attrs().Name, nil - } - } - return "", ErrNetworkDisconnected -} - func (m *linuxNetworkManager) CreateInterface(interfaceName string) error { m.mu.Lock() defer m.mu.Unlock() @@ -66,8 +39,8 @@ func (m *linuxNetworkManager) RemoveInterface(interfaceName string) error { return nil } -func NewNetworkManager(bridgeName, subnet string) (NetworkManager, error) { - if bridgeErr := ifc.CreateBridge(bridgeName, subnet, true); bridgeErr != nil { +func NewNetworkManager(bridgeName, gateway string) (NetworkManager, error) { + if bridgeErr := ifc.CreateBridge(bridgeName, gateway, true); bridgeErr != nil { return nil, bridgeErr } @@ -77,64 +50,5 @@ func NewNetworkManager(bridgeName, subnet string) (NetworkManager, error) { events: make(chan Event, 10), } - currentDefaultInterface := "" - go func() { - for event := range nm.events { - switch ev := event.(type) { - case *DefaultInterfaceChanged: - if firewallErr := firewall.ConfigureFirewall(ev.OldInterface, - ev.NewInterface, - bridgeName); firewallErr == nil { - } else { - slog.Error("could not configure firewall", "new interface", ev.NewInterface, "error", firewallErr) - } - } - } - }() - - updates := make(chan netlink.LinkUpdate) - - subscribeErr := netlink.LinkSubscribe(updates, nm.done) - if subscribeErr != nil { - return nil, subscribeErr - } - - if defaultInterface, defaultInterfaceErr := getDefaultInterface(); defaultInterfaceErr == nil && defaultInterface != "" { - nm.events <- &DefaultInterfaceChanged{ - NewInterface: defaultInterface, - OldInterface: currentDefaultInterface, - } - currentDefaultInterface = defaultInterface - } else { - return nil, defaultInterfaceErr - } - - go func() { - outerloop: - for { - select { - case <-updates: - newDefaultInterface := "" - if defaultInterface, defaultInterfaceErr := getDefaultInterface(); defaultInterfaceErr == nil { - newDefaultInterface = defaultInterface - } - if currentDefaultInterface != newDefaultInterface { - if newDefaultInterface == "" { - slog.Debug("Got disconnected from the internet") - } else { - slog.Debug("New default interface was configured", "interface", newDefaultInterface) - } - nm.events <- &DefaultInterfaceChanged{ - NewInterface: newDefaultInterface, - OldInterface: currentDefaultInterface, - } - currentDefaultInterface = newDefaultInterface - } - case <-nm.done: - break outerloop - } - } - }() - return nm, nil } diff --git a/src/protos/settings/v1/settings.proto b/src/protos/settings/v1/settings.proto index 8238ddb..d2b6aeb 100644 --- a/src/protos/settings/v1/settings.proto +++ b/src/protos/settings/v1/settings.proto @@ -14,14 +14,23 @@ message Cache { string root = 1; } -message Bridge { +message Dhcp { + string start = 1; + string end = 2; + repeated string dns = 3; + int64 lease_time = 4; + string lease_file = 5; +} + +message Network { string name = 1; - string subnet = 2; - string gateway = 3; + string gateway_ip = 2; + string bridge_ip = 3; + Dhcp dhcp = 4; } message LinuxSettings { - Bridge bridge = 1; + Network network = 1; } message ControllerConfig { diff --git a/src/qcontrollerd/cmd/config_linux.go b/src/qcontrollerd/cmd/config_linux.go new file mode 100644 index 0000000..bbcf8b7 --- /dev/null +++ b/src/qcontrollerd/cmd/config_linux.go @@ -0,0 +1,69 @@ +package cmd + +import ( + "errors" + "fmt" + "net" + + settingsv1 "github.com/q-controller/qcontroller/src/generated/settings/v1" +) + +type LinuxConfig struct { + Name string + BridgeIp net.IP + GatewayIp net.IP + StartIp net.IP + EndIp net.IP + Subnet *net.IPNet +} + +func NewLinuxConfig(settings *settingsv1.LinuxSettings) (*LinuxConfig, error) { + if settings == nil { + return nil, errors.New("linux settings cannot be nil") + } + + hostIp, hostNet, hostErr := net.ParseCIDR(settings.Network.GatewayIp) + if hostErr != nil { + return nil, fmt.Errorf("failed to parse host_ip %s: %w", settings.Network.GatewayIp, hostErr) + } + + bridgeIp, bridgeNet, bridgeErr := net.ParseCIDR(settings.Network.BridgeIp) + if bridgeErr != nil { + return nil, fmt.Errorf("failed to parse bridge_ip %s: %w", settings.Network.BridgeIp, bridgeErr) + } + + if bridgeNet.String() != hostNet.String() { + return nil, fmt.Errorf("bridge (%s) and host (%s) subnets are not same", bridgeNet.String(), hostNet.String()) + } + + if bridgeIp.Equal(hostIp) { + return nil, fmt.Errorf("bridge_ip (%s) cannot be same as host_ip (%s)", bridgeIp.String(), hostIp.String()) + } + + startIp, startNet, startErr := net.ParseCIDR(settings.Network.Dhcp.Start) + if startErr != nil { + return nil, fmt.Errorf("failed to parse dhcp_start %s: %w", settings.Network.Dhcp.Start, startErr) + } + + endIp, endNet, endErr := net.ParseCIDR(settings.Network.Dhcp.End) + if endErr != nil { + return nil, fmt.Errorf("failed to parse dhcp_end %s: %w", settings.Network.Dhcp.End, endErr) + } + + if startNet.String() != hostNet.String() { + return nil, fmt.Errorf("dhcp_start (%s) and network (%s) subnets are not same", startNet.String(), hostNet.String()) + } + + if endNet.String() != hostNet.String() { + return nil, fmt.Errorf("dhcp_end (%s) and network (%s) subnets are not same", endNet.String(), hostNet.String()) + } + + return &LinuxConfig{ + BridgeIp: bridgeIp, + GatewayIp: hostIp, + Subnet: hostNet, + Name: settings.Network.Name, + StartIp: startIp, + EndIp: endIp, + }, nil +} diff --git a/src/qcontrollerd/cmd/qemu.go b/src/qcontrollerd/cmd/qemu.go deleted file mode 100644 index f4fc620..0000000 --- a/src/qcontrollerd/cmd/qemu.go +++ /dev/null @@ -1,125 +0,0 @@ -package cmd - -import ( - "context" - "errors" - "fmt" - "log/slog" - "net" - "os" - "os/signal" - "syscall" - "time" - - servicesv1 "github.com/q-controller/qcontroller/src/generated/services/v1" - settingsv1 "github.com/q-controller/qcontroller/src/generated/settings/v1" - "github.com/q-controller/qcontroller/src/pkg/protos" - "github.com/q-controller/qcontroller/src/pkg/qemu/process" - "github.com/q-controller/qcontroller/src/pkg/utils" - "github.com/spf13/cobra" - "google.golang.org/grpc" -) - -var qemuCmd = &cobra.Command{ - Use: "qemu", - Short: "Starts QEMU client", - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - if os.Geteuid() != 0 { - return errors.New("this command must be run as root") - } - return nil - }, - RunE: func(cmd *cobra.Command, args []string) error { - configPath, configPathErr := cmd.Flags().GetString("config") - if configPathErr != nil { - return fmt.Errorf("failed to get config: %w", configPathErr) - } - - config := &settingsv1.QemuConfig{} - if unmarshalErr := utils.Unmarshal(config, configPath); unmarshalErr != nil { - return unmarshalErr - } - slog.Debug("Read config", "config", config) - - lis, lisErr := net.Listen("tcp", fmt.Sprintf(":%d", config.Port)) - if lisErr != nil { - return fmt.Errorf("failed to listen: %w", lisErr) - } - defer func() { - if err := lis.Close(); err != nil { - slog.Error("Failed to close the listener", "error", err) - } - }() - - s := grpc.NewServer() - defer func() { - // Create a deadline for graceful shutdown - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) - defer shutdownCancel() - - // Give ongoing operations a chance to complete - done := make(chan struct{}) - go func() { - s.GracefulStop() // Stop accepting new requests, let existing ones finish - close(done) - }() - - // Wait for graceful shutdown or timeout - select { - case <-shutdownCtx.Done(): - slog.Warn("Graceful shutdown timed out, forcing stop") - s.Stop() - case <-done: - slog.Info("Graceful shutdown completed") - } - }() - - monitor, monitorErr := process.NewInstanceMonitor() - if monitorErr != nil { - return monitorErr - } - defer func() { - if err := monitor.Close(); err != nil { - slog.Error("Error closing monitor", "error", err) - } - }() - - reg, regErr := protos.NewQemuService(monitor, config) - if regErr != nil { - return fmt.Errorf("failed to create server %w", regErr) - } - servicesv1.RegisterQemuServiceServer(s, reg) - - stop := make(chan os.Signal, 1) - signal.Notify(stop, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) - - // Server error channel - errCh := make(chan error, 1) - - // Start server in background - go func() { - if err := s.Serve(lis); err != nil { - errCh <- err - } - }() - - // Wait for signal or error - select { - case <-stop: - slog.Info("Shutting down gRPC server due to signal...") - case err := <-errCh: - return err - } - - return nil - }, -} - -func init() { - rootCmd.AddCommand(qemuCmd) - - qemuCmd.Flags().StringP("config", "c", "", "Path to qemu's config file") - if err := qemuCmd.MarkFlagRequired("config"); err != nil { - panic(fmt.Errorf("failed to mark flag `config` as required: %w", err)) - } -} diff --git a/src/qcontrollerd/cmd/qemu_common.go b/src/qcontrollerd/cmd/qemu_common.go new file mode 100644 index 0000000..3c274d3 --- /dev/null +++ b/src/qcontrollerd/cmd/qemu_common.go @@ -0,0 +1,86 @@ +package cmd + +import ( + "context" + "fmt" + "log/slog" + "net" + "time" + + servicesv1 "github.com/q-controller/qcontroller/src/generated/services/v1" + settingsv1 "github.com/q-controller/qcontroller/src/generated/settings/v1" + "github.com/q-controller/qcontroller/src/pkg/protos" + "github.com/q-controller/qcontroller/src/pkg/qemu/process" + "google.golang.org/grpc" +) + +func Entrypoint(config *settingsv1.QemuConfig, stop <-chan struct{}) error { + lis, lisErr := net.Listen("tcp", fmt.Sprintf(":%d", config.Port)) + if lisErr != nil { + return fmt.Errorf("failed to listen: %w", lisErr) + } + defer func() { + if err := lis.Close(); err != nil { + slog.Error("Failed to close the listener", "error", err) + } + }() + + s := grpc.NewServer() + defer func() { + // Create a deadline for graceful shutdown + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer shutdownCancel() + + // Give ongoing operations a chance to complete + done := make(chan struct{}) + go func() { + s.GracefulStop() // Stop accepting new requests, let existing ones finish + close(done) + }() + + // Wait for graceful shutdown or timeout + select { + case <-shutdownCtx.Done(): + slog.Warn("Graceful shutdown timed out, forcing stop") + s.Stop() + case <-done: + slog.Info("Graceful shutdown completed") + } + }() + + monitor, monitorErr := process.NewInstanceMonitor() + if monitorErr != nil { + return monitorErr + } + defer func() { + if err := monitor.Close(); err != nil { + slog.Error("Error closing monitor", "error", err) + } + }() + + reg, regErr := protos.NewQemuService(monitor, config) + if regErr != nil { + return fmt.Errorf("failed to create server %w", regErr) + } + servicesv1.RegisterQemuServiceServer(s, reg) + + // Server error channel + errCh := make(chan error, 1) + + // Start server in background + go func() { + if err := s.Serve(lis); err != nil { + errCh <- err + } + }() + + // Wait for signal or error + select { + case <-stop: + slog.Info("Shutting down gRPC server due to signal...") + case err := <-errCh: + return err + } + + return nil +} diff --git a/src/qcontrollerd/cmd/qemu_darwin.go b/src/qcontrollerd/cmd/qemu_darwin.go new file mode 100644 index 0000000..f1ec6f8 --- /dev/null +++ b/src/qcontrollerd/cmd/qemu_darwin.go @@ -0,0 +1,63 @@ +package cmd + +import ( + "errors" + "fmt" + "log/slog" + "os" + "os/signal" + "syscall" + + settingsv1 "github.com/q-controller/qcontroller/src/generated/settings/v1" + "github.com/q-controller/qcontroller/src/pkg/utils" + + "github.com/spf13/cobra" +) + +var qemuCmd = &cobra.Command{ + Use: "qemu", + Short: "Starts QEMU client", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if os.Geteuid() != 0 { + return errors.New("this command must be run as root") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + configPath, configPathErr := cmd.Flags().GetString("config") + if configPathErr != nil { + return fmt.Errorf("failed to get config: %w", configPathErr) + } + + config := &settingsv1.QemuConfig{} + if unmarshalErr := utils.Unmarshal(config, configPath); unmarshalErr != nil { + return unmarshalErr + } + slog.Debug("Read config", "config", config) + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) + + done := make(chan struct{}) + go func() { + <-stop + close(done) + }() + + return Entrypoint(config, done) + }, +} + +func init() { + rootCmd.AddCommand(qemuCmd) + + qemuCmd.Flags().StringP("config", "c", "", "Path to qemu's config file") + if err := qemuCmd.MarkFlagRequired("config"); err != nil { + panic(fmt.Errorf("failed to mark flag `config` as required: %w", err)) + } + + qemuCmd.Flags().Bool("in-namespace", false, "Run QEMU inside network namespace") + if err := qemuCmd.Flags().MarkHidden("in-namespace"); err != nil { + panic(fmt.Errorf("failed to mark flag `in-namespace` as hidden: %w", err)) + } +} diff --git a/src/qcontrollerd/cmd/qemu_linux.go b/src/qcontrollerd/cmd/qemu_linux.go new file mode 100644 index 0000000..ccc7243 --- /dev/null +++ b/src/qcontrollerd/cmd/qemu_linux.go @@ -0,0 +1,149 @@ +package cmd + +import ( + "errors" + "fmt" + "log/slog" + "net" + "os" + "os/exec" + "os/signal" + "syscall" + + "github.com/q-controller/network-utils/src/utils/network/dhcp" + "github.com/q-controller/network-utils/src/utils/network/ifc" + "github.com/q-controller/network-utils/src/utils/network/network" + settingsv1 "github.com/q-controller/qcontroller/src/generated/settings/v1" + "github.com/q-controller/qcontroller/src/pkg/utils" + + "github.com/spf13/cobra" +) + +var qemuCmd = &cobra.Command{ + Use: "qemu", + Short: "Starts QEMU client", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if os.Geteuid() != 0 { + return errors.New("this command must be run as root") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + configPath, configPathErr := cmd.Flags().GetString("config") + if configPathErr != nil { + return fmt.Errorf("failed to get config: %w", configPathErr) + } + + config := &settingsv1.QemuConfig{} + if unmarshalErr := utils.Unmarshal(config, configPath); unmarshalErr != nil { + return unmarshalErr + } + slog.Debug("Read config", "config", config) + + linuxConfig, linuxConfigErr := NewLinuxConfig(config.GetLinuxSettings()) + if linuxConfigErr != nil { + return fmt.Errorf("failed to create linux config: %w", linuxConfigErr) + } + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) + + done := make(chan struct{}) + go func() { + <-stop + close(done) + }() + + inNamespace, inNamespaceErr := cmd.Flags().GetBool("in-namespace") + if inNamespaceErr != nil { + return fmt.Errorf("failed to get in-namespace flag: %w", inNamespaceErr) + } + + if inNamespace { + dns := []net.IP{} + for _, ipStr := range config.GetLinuxSettings().Network.Dhcp.Dns { + ip := net.ParseIP(ipStr) + if ip == nil { + return fmt.Errorf("failed to parse dns ip %s", ipStr) + } + dns = append(dns, ip) + } + + dhcpServer, dhcpServerErr := dhcp.StartDHCPServer( + dhcp.WithDNS(dns...), + dhcp.WithInterface(linuxConfig.Name, linuxConfig.BridgeIp), + dhcp.WithLeaseFile(config.GetLinuxSettings().Network.Dhcp.LeaseFile), + dhcp.WithRange(linuxConfig.StartIp, linuxConfig.EndIp), + ) + if dhcpServerErr != nil { + return fmt.Errorf("failed to start DHCP server: %w", dhcpServerErr) + } + defer dhcpServer.Stop() + + return Entrypoint(config, done) + } else { + netw, netwErr := network.NewNetwork( + network.WithName(linuxConfig.Name), + network.WithGateway(linuxConfig.GatewayIp), + network.WithBridge(linuxConfig.BridgeIp), + network.WithSubnet(linuxConfig.Subnet), + network.WithLinkManager(ifc.NetlinkBridgeManager{}), + ) + if netwErr != nil { + return fmt.Errorf("failed to create network: %w", netwErr) + } + + subscription, subscribeErr := ifc.SubscribeDefaultInterfaceChanges() + if subscribeErr != nil { + return fmt.Errorf("failed to subscribe to default interface changes: %w", subscribeErr) + } + defer subscription.Stop() + + go func() { + for iface := range subscription.InterfaceCh { + slog.Info("Default interface changed", "interface", iface) + if err := netw.Connect(iface); err != nil { + slog.Error("Failed to connect network to interface", "interface", iface, "error", err) + } + } + }() + + args := append([]string(nil), os.Args[1:]...) // clone without program name + args = append(args, "--in-namespace", linuxConfig.Name) + + cmd := exec.Command(os.Args[0], args...) + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + if err := netw.Execute(func() error { + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start qemu command: %w", err) + } + return nil + }); err != nil { + return fmt.Errorf("failed to execute network command: %w", err) + } + + <-done + + slog.Info("Stopping QEMU process") + if err := cmd.Process.Signal(os.Interrupt); err != nil { + slog.Error("Failed to send interrupt to QEMU process", "error", err) + } + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(qemuCmd) + + qemuCmd.Flags().StringP("config", "c", "", "Path to qemu's config file") + if err := qemuCmd.MarkFlagRequired("config"); err != nil { + panic(fmt.Errorf("failed to mark flag `config` as required: %w", err)) + } + + qemuCmd.Flags().Bool("in-namespace", false, "Run QEMU inside network namespace") + if err := qemuCmd.Flags().MarkHidden("in-namespace"); err != nil { + panic(fmt.Errorf("failed to mark flag `in-namespace` as hidden: %w", err)) + } +} diff --git a/start.sh b/start.sh index a510d7e..34cc663 100755 --- a/start.sh +++ b/start.sh @@ -5,7 +5,10 @@ set -Eeuo pipefail trap cleanup SIGINT SIGTERM ERR EXIT INTERFACE_NAME=br0 -SUBNET=192.168.71.0/24 +HOST_IP=192.168.71.1/24 +BRIDGE_IP=192.168.71.3/24 +START=192.168.71.4/24 +END=192.168.71.254/24 CONTROLLER_PORT=8009 QEMU_PORT=8008 GATEWAY_PORT=8080 @@ -29,7 +32,9 @@ Available options: --bin Path to qcontrollerd binary --rundir Path to the rundir --interface Interface name [default: ${INTERFACE_NAME}] (Linux only) ---cidr Subnet [default: ${SUBNET}] (Linux only) +--cidr Gateway CIDR [default: ${GATEWAY}] (Linux only) +--start Start of DHCP range (Linux only) [default: ${START}] +--end End of DHCP range (Linux only) [default: ${END}] EOF exit } @@ -62,7 +67,19 @@ parse_params() { ;; --cidr) if [[ "$OS_TYPE" == "Linux" ]]; then - SUBNET="${2-}" + GATEWAY="${2-}" + fi + shift + ;; + --start) + if [[ "$OS_TYPE" == "Linux" ]]; then + START="${2-}" + fi + shift + ;; + --end) + if [[ "$OS_TYPE" == "Linux" ]]; then + END="${2-}" fi shift ;; @@ -103,9 +120,17 @@ cat >${CONFIGDIR}/qemu-config.json <${CONFIGDIR}/controller-config.json < Date: Fri, 28 Nov 2025 19:39:39 +0100 Subject: [PATCH 2/8] Remove dhcp scripts DHCP server is built in now. Signed-off-by: Nikita Vakula --- dhcp/Dockerfile | 7 ---- dhcp/README.md | 11 ----- dhcp/dhcp-entrypoint.sh | 89 ----------------------------------------- 3 files changed, 107 deletions(-) delete mode 100644 dhcp/Dockerfile delete mode 100644 dhcp/README.md delete mode 100755 dhcp/dhcp-entrypoint.sh diff --git a/dhcp/Dockerfile b/dhcp/Dockerfile deleted file mode 100644 index 0fff0f7..0000000 --- a/dhcp/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM alpine:3.20 AS dhcp - -RUN apk add --no-cache dnsmasq bash iptables - -ADD dhcp-entrypoint.sh /usr/bin/dhcp-entrypoint.sh - -ENTRYPOINT ["/usr/bin/dhcp-entrypoint.sh"] diff --git a/dhcp/README.md b/dhcp/README.md deleted file mode 100644 index c8a0787..0000000 --- a/dhcp/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Run DHCP - -```shell -docker run --rm -d \ - --cap-add NET_ADMIN \ - --network host \ - --privileged \ - --device /dev/net/tun:/dev/net/tun \ - $(docker build -q .) \ - -n "${INTERFACE_NAME}" -``` diff --git a/dhcp/dhcp-entrypoint.sh b/dhcp/dhcp-entrypoint.sh deleted file mode 100755 index 81e97f4..0000000 --- a/dhcp/dhcp-entrypoint.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env bash - -set -Eeuo pipefail -trap cleanup SIGINT SIGTERM ERR EXIT - -script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) -INTERFACE_NAME="krjakbrjakbr0" -LOCATION=$(mktemp -d) - -usage() { - cat <&2 -e "$msg" - exit "$code" -} - -parse_params() { - while :; do - case "${1-}" in - -h | --help) usage ;; - -v | --verbose) set -x ;; - -n | --name) - INTERFACE_NAME="${2-}" - shift - ;; - -?*) die "Unknown option: $1" ;; - *) break ;; - esac - shift - done - - args=("$@") - - return 0 -} - -parse_params "$@" - -until ip link show ${INTERFACE_NAME} 2>/dev/null; do - echo "Waiting for ${INTERFACE_NAME} to be CREATED..." - sleep 1 -done - -while true; do - IP_INFO=$(ip -o -f inet addr show "${INTERFACE_NAME}" | awk '{print $4}') - if [ -n "${IP_INFO}" ]; then - IFS='./' read -r a b c d mask <<< "${IP_INFO}" - cat < ${LOCATION}/dnsmasq.conf -port=0 # Disable DNS server -interface=${INTERFACE_NAME} -bind-interfaces -listen-address=${a}.${b}.${c}.${d} -dhcp-range=${a}.${b}.${c}.$((d+1)),${a}.${b}.${c}.$((d+100)),255.255.255.0,12h -dhcp-option=3,${a}.${b}.${c}.${d} # Gateway -dhcp-option=6,8.8.8.8,1.1.1.1 # DNS -log-queries -log-dhcp -EOF - break - fi -done - -until ip link show ${INTERFACE_NAME} | grep -q 'state UP'; do - echo "Waiting for ${INTERFACE_NAME} to be UP..." - sleep 1 -done - -exec dnsmasq -k --conf-file=${LOCATION}/dnsmasq.conf --dhcp-ignore-clid --dhcp-authoritative --dhcp-no-override From 3db4547c4e2579316d3b62b29a28e247f2c1affa Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Fri, 28 Nov 2025 19:39:26 +0100 Subject: [PATCH 3/8] Adjust readme Signed-off-by: Nikita Vakula --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 2d41a7c..a3178cd 100644 --- a/README.md +++ b/README.md @@ -124,9 +124,3 @@ This wraps the environment with all Go tools and build dependencies preinstalled - `qemu-system-x86_64` (x86_64 VMs are supported and tested) - `qemu-system-aarch64` (ARM64 VMs are supported and tested) - -## 🔧 Networking Notes - -On Linux, a DHCP server is required for automated VM networking (e.g., bridge + TAP). For setup and an example configuration, see [dhcp](/dhcp/README.md). - -> **Note**: DHCP is not bundled with this project to give users the freedom to plug into their existing setup From 6a1fdbb8c5fc911e8f827d1d8fe68423552f9891 Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Fri, 28 Nov 2025 13:42:48 +0100 Subject: [PATCH 4/8] Remove unused code Signed-off-by: Nikita Vakula --- src/pkg/protos/networkmanager_linux.go | 39 --------------------- src/protos/services/v1/networkmanager.proto | 20 ----------- 2 files changed, 59 deletions(-) delete mode 100644 src/pkg/protos/networkmanager_linux.go delete mode 100644 src/protos/services/v1/networkmanager.proto diff --git a/src/pkg/protos/networkmanager_linux.go b/src/pkg/protos/networkmanager_linux.go deleted file mode 100644 index 191d2bf..0000000 --- a/src/pkg/protos/networkmanager_linux.go +++ /dev/null @@ -1,39 +0,0 @@ -package protos - -import ( - "context" - - servicesv1 "github.com/q-controller/qcontroller/src/generated/services/v1" - settingsv1 "github.com/q-controller/qcontroller/src/generated/settings/v1" - "github.com/q-controller/qcontroller/src/pkg/utils/network" - emptypb "google.golang.org/protobuf/types/known/emptypb" -) - -type NetworkManagerServer struct { - servicesv1.UnimplementedNetworkManagerServiceServer - - nm network.NetworkManager -} - -func (s *NetworkManagerServer) CreateInterface(ctx context.Context, - req *servicesv1.CreateInterfaceRequest) (*emptypb.Empty, error) { - return &emptypb.Empty{}, s.nm.CreateInterface(req.Name) -} - -func (s *NetworkManagerServer) RemoveInterface(ctx context.Context, - req *servicesv1.RemoveInterfaceRequest) (*emptypb.Empty, error) { - return &emptypb.Empty{}, s.nm.RemoveInterface(req.Name) -} - -func NewNetworkManager(bridge *settingsv1.Bridge) (servicesv1.NetworkManagerServiceServer, error) { - nm, nmErr := network.NewNetworkManager(bridge.Name, bridge.Subnet) - if nmErr != nil { - return nil, nmErr - } - - server := &NetworkManagerServer{ - nm: nm, - } - - return server, nil -} diff --git a/src/protos/services/v1/networkmanager.proto b/src/protos/services/v1/networkmanager.proto deleted file mode 100644 index ddaef71..0000000 --- a/src/protos/services/v1/networkmanager.proto +++ /dev/null @@ -1,20 +0,0 @@ -syntax = "proto3"; - -package services.v1; - -import "google/protobuf/empty.proto"; - -option go_package = "github.com/q-controller/qcontroller/src/generated/services/v1;v1"; - -message CreateInterfaceRequest { - string name = 1; -} - -message RemoveInterfaceRequest { - string name = 1; -} - -service NetworkManagerService { - rpc CreateInterface(CreateInterfaceRequest) returns (google.protobuf.Empty) {} - rpc RemoveInterface(RemoveInterfaceRequest) returns (google.protobuf.Empty) {} -} From 7d038d4b6638669b9e8ff34b880c24ea413b9419 Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Fri, 28 Nov 2025 14:33:18 +0100 Subject: [PATCH 5/8] fix: validate QMP greeting message before processing Signed-off-by: Nikita Vakula --- src/pkg/qemu/process/instancemonitor.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pkg/qemu/process/instancemonitor.go b/src/pkg/qemu/process/instancemonitor.go index 022801a..dbd0b2f 100644 --- a/src/pkg/qemu/process/instancemonitor.go +++ b/src/pkg/qemu/process/instancemonitor.go @@ -164,6 +164,11 @@ func NewInstanceMonitor() (*InstanceMonitor, error) { if msg.Generic != nil { var greeting Greeting if err := json.Unmarshal(msg.Generic, &greeting); err == nil { + // Verify that the unmarshaled greeting has the expected QMP structure + if greeting.QMP.Version.Qemu.Major == 0 && greeting.QMP.Version.Qemu.Minor == 0 && greeting.QMP.Version.Qemu.Micro == 0 { + continue // Invalid greeting structure + } + if req, reqErr := qapi.PrepareQmpCapabilitiesRequest(qapi.QObjQmpCapabilitiesArg{}); reqErr == nil { if ch, chErr := qapiClient.Execute(msg.Instance, client.Request(*req)); chErr == nil { res, resOk := ch.Get(context.Background(), -1) From d613c1ca07a16567ad5534980a9f5fcfb3b716f8 Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Thu, 27 Nov 2025 17:56:33 +0100 Subject: [PATCH 6/8] Update qapi-client submodule Signed-off-by: Nikita Vakula --- qapi-client | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qapi-client b/qapi-client index 6fa9e16..8321b74 160000 --- a/qapi-client +++ b/qapi-client @@ -1 +1 @@ -Subproject commit 6fa9e16ef14536cec423d7bf73b667b7eb97b001 +Subproject commit 8321b748a06921916e75bf459657f10b60b6b9cd From 1c16859519ef3b32899d94c7c97bba0a1e33dae4 Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Thu, 27 Nov 2025 10:31:04 +0100 Subject: [PATCH 7/8] Replace coredhcp with its fork This fork implements DHCP RELEASE message handling Signed-off-by: Nikita Vakula --- go.mod | 2 ++ go.sum | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index c5143de..0c5cd61 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,8 @@ replace github.com/q-controller/qemu-client => ./qemu-client replace github.com/q-controller/qapi-client => ./qapi-client +replace github.com/coredhcp/coredhcp => github.com/krjakbrjak/coredhcp v0.0.0-20251128103901-a2aa0dc11269 + require ( github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.0 diff --git a/go.sum b/go.sum index bf2c4c0..250734c 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb h1:aZTKxMminKeQWHtzJBbV8TttfTxzdJ+7iEJFE6FmUzg= github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb/go.mod h1:xzXc1S/L+64uglB3pw54o8kqyM6KFYpTeC9Q6+qZIu8= -github.com/coredhcp/coredhcp v0.0.0-20250927164030-d2ed887fca9b h1:L9ffVtIOGfIuduN7ZY7E0Z8rI2vbcwizZytbNrm5mHs= -github.com/coredhcp/coredhcp v0.0.0-20250927164030-d2ed887fca9b/go.mod h1:+FFQuUBeTgdrHkY6EyWdG3jm3jzt2dGrjl8amnmyoDw= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -76,6 +74,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/krjakbrjak/coredhcp v0.0.0-20251128103901-a2aa0dc11269 h1:btYwynwpjVZ8FNtgWGvWGSzyFoZb0NdQp3Wkaa0gR4o= +github.com/krjakbrjak/coredhcp v0.0.0-20251128103901-a2aa0dc11269/go.mod h1:1yk/ElKGa71B3YnjAeBhHuQTnzJk82A0ab/u2PVbDok= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= From 56e8b6ec053968ed10e270dfbb70172ecf20fc88 Mon Sep 17 00:00:00 2001 From: Nikita Vakula Date: Sat, 29 Nov 2025 19:25:31 +0100 Subject: [PATCH 8/8] Fix start script Signed-off-by: Nikita Vakula --- start.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/start.sh b/start.sh index 34cc663..2ffe69d 100755 --- a/start.sh +++ b/start.sh @@ -143,6 +143,11 @@ cat >${CONFIGDIR}/qemu-config.json <${CONFIGDIR}/controller-config.json <${CONFIGDIR}/controller-config.json <