diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 169d2f5..8d5dd88 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:12-slim as dev +FROM debian:12-slim RUN apt -y update && \ apt -y upgrade && \ @@ -9,7 +9,7 @@ RUN apt -y update && \ inotify-tools curl wget ifupdown2 jq build-essential git \ unzip zip cmake automake autoconf libtool autopoint gettext \ tar gzip zsh vim nano sudo zstd less gnupg ripgrep gdb cgdb locales \ - systemd systemd-resolved systemd-sysv + systemd systemd-resolved systemd-sysv dbus iproute2 iputils-ping RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \ sed -i -e 's/# fr_FR.UTF-8 UTF-8/fr_FR.UTF-8 UTF-8/' /etc/locale.gen && \ @@ -39,4 +39,25 @@ RUN echo "\n\ \n\ set show-all-if-ambiguous on \n\ set completion-ignore-case on \n\ -" >> /etc/inputrc \ No newline at end of file +" >> /etc/inputrc + +RUN echo "\n\ +[Match]\n\ +Name=calaos-*\n\ +\n\ +[Network]\n\ +DHCP=yes\n\ +" > /etc/systemd/network/calaos.network + +COPY setup_veth.sh /usr/local/bin/setup_veth.sh +RUN chmod +x /usr/local/bin/setup_veth.sh +COPY setup-veth.service /etc/systemd/system/setup-veth.service +RUN systemctl enable setup-veth.service + +RUN systemctl enable setup-veth.service +RUN systemctl enable systemd-resolved.service +RUN systemctl enable systemd-networkd.service + +ENV container docker +STOPSIGNAL SIGRTMIN+3 +CMD ["/lib/systemd/systemd", "--system", "--unit=basic.target"] \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 643ba1b..5d1c3da 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -19,10 +19,16 @@ "customizations": { "vscode": { "extensions": [ - "ms-vscode.cpptools-extension-pack", - "ms-vscode.makefile-tools", - "lizebang.bash-extension-pack", - "ms-vscode.cmake-tools" + "ms-vscode.cpptools-extension-pack", + "ms-vscode.makefile-tools", + "lizebang.bash-extension-pack", + "ms-vscode.cmake-tools", + "ms-azuretools.vscode-docker", + "golang.go", + "jinliming2.vscode-go-template", + "mesonbuild.mesonbuild", + "esbenp.prettier-vscode", + "foxundermoon.shell-format" ] } }, @@ -30,5 +36,14 @@ // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. "remoteUser": "root", - "runArgs": ["--privileged"] + "runArgs": [ + "--privileged", + "--cap-add=SYS_ADMIN", + "--tmpfs", "/run", + "--tmpfs", "/run/lock", + "-v", "/sys/fs/cgroup:/sys/fs/cgroup" + ], + + "overrideCommand": false, + "privileged": true } \ No newline at end of file diff --git a/.devcontainer/setup-veth.service b/.devcontainer/setup-veth.service new file mode 100644 index 0000000..e79aaf6 --- /dev/null +++ b/.devcontainer/setup-veth.service @@ -0,0 +1,11 @@ +[Unit] +Description=Setup virtual ethernet interfaces (veth) +Before=systemd-networkd.service + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/setup_veth.sh +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target diff --git a/.devcontainer/setup_veth.sh b/.devcontainer/setup_veth.sh new file mode 100755 index 0000000..ae19725 --- /dev/null +++ b/.devcontainer/setup_veth.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Create 4 pairs of veth interfaces + +for i in {0..1}; do + veth_in="calaos-${i}-0" + veth_out="calaos-${i}-1" + + ip link add "$veth_in" type veth peer name "$veth_out" + ip link set "$veth_in" up + ip link set "$veth_out" up +done diff --git a/app/app.go b/app/app.go index 3161517..39958e3 100644 --- a/app/app.go +++ b/app/app.go @@ -99,6 +99,10 @@ func NewApp() (a *AppServer, err error) { return a.apiNetIntfList(c) }) + api.Get("/network/dns", func(c *fiber.Ctx) error { + return a.apiNetDNS(c) + }) + //Force an update check api.Get("/update/check", func(c *fiber.Ctx) error { return a.apiUpdateCheck(c) diff --git a/app/network.go b/app/network.go index 62c7950..1cc45ba 100644 --- a/app/network.go +++ b/app/network.go @@ -21,3 +21,19 @@ func (a *AppServer) apiNetIntfList(c *fiber.Ctx) (err error) { "output": nets, }) } + +func (a *AppServer) apiNetDNS(c *fiber.Ctx) (err error) { + dns, err := models.GetDNSConfig() + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ + "error": true, + "msg": err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(fiber.Map{ + "error": false, + "msg": "ok", + "output": dns, + }) +} diff --git a/debian/usb-serial-touchscreen@.service b/debian/usb-serial-touchscreen@.service index c20be4a..6f9d7aa 100644 --- a/debian/usb-serial-touchscreen@.service +++ b/debian/usb-serial-touchscreen@.service @@ -1,6 +1,6 @@ [Unit] Description=Inputattach tool for serial2USB converter on %i -BindTo=dev-%i.device +BindsTo=dev-%i.device After=dev-%i.device xserver-nodm.service [Service] diff --git a/models/network.go b/models/network.go index 4b53a6f..ed56830 100644 --- a/models/network.go +++ b/models/network.go @@ -1,49 +1,153 @@ package models -import "net" +import ( + "bufio" + "encoding/json" + "fmt" + "os/exec" + "regexp" + "strings" +) type NetIntf struct { Name string `json:"name"` IPv4 string `json:"ipv4"` + Mask string `json:"mask"` + Gateway string `json:"gateway"` IPv6 string `json:"ipv6"` MAC string `json:"mac"` IsLoopback bool `json:"is_loopback"` + State string `json:"state"` +} + +type DNSConfig struct { + Interface string `json:"interface"` + DNSServers []string `json:"dns_servers"` + SearchDomains []string `json:"search_domains"` +} + +type RawNetInterface struct { + Name string `json:"Name"` + Flags int `json:"Flags"` + IPv4 string `json:"IPv4Address"` + IPv4Mask int `json:"IPv4Mask"` + IPv6 string `json:"IPv6LinkLocalAddress"` + MAC string `json:"HardwareAddress"` + State string `json:"KernelOperationalStateString"` + Addresses []struct { + Family int `json:"Family"` + Scope string `json:"ScopeString"` + Prefix int `json:"PrefixLength"` + } `json:"Addresses"` + Routes []struct { + Family int `json:"Family"` + Type string `json:"TypeString"` + Gateway string `json:"Gateway"` + Dest string `json:"Destination"` + PrefixLen int `json:"DestinationPrefixLength"` + } `json:"Routes"` +} + +type RawDNSConfig struct { + Link int `json:"Link"` + CurrentDNS []string `json:"CurrentDNSServer"` + SearchDomains []string `json:"Domains"` + LLMNR string `json:"LLMNR"` + MulticastDNS string `json:"MulticastDNS"` + DNSSEC string `json:"DNSSEC"` + InterfaceName string `json:"InterfaceName"` +} + +func parseMask(prefixLen int) string { + mask := (0xFFFFFFFF << (32 - prefixLen)) & 0xFFFFFFFF + return fmt.Sprintf("%d.%d.%d.%d", (mask>>24)&0xFF, (mask>>16)&0xFF, (mask>>8)&0xFF, mask&0xFF) } func GetAllNetInterfaces() (nets []*NetIntf, err error) { - ifaces, err := net.Interfaces() + cmd := exec.Command("/usr/bin/networkctl", "list", "--json=short") + output, err := cmd.Output() if err != nil { return nil, err } - for _, iface := range ifaces { - addrs, err := iface.Addrs() - if err != nil { - return nil, err + var rawInterfaces []RawNetInterface + if err := json.Unmarshal(output, &rawInterfaces); err != nil { + return nil, err + } + + for _, rawIntf := range rawInterfaces { + netIntf := &NetIntf{ + Name: rawIntf.Name, + MAC: rawIntf.MAC, + State: rawIntf.State, + IsLoopback: (rawIntf.Flags & 0x8) != 0, // check IFF_LOOPBACK } - intf := &NetIntf{ - Name: iface.Name, - IsLoopback: iface.Flags&net.FlagLoopback != 0, - MAC: iface.HardwareAddr.String(), + // Look for IPv4 address and mask + for _, addr := range rawIntf.Addresses { + if addr.Family == 2 { // IPv4 + netIntf.IPv4 = fmt.Sprintf("%d.%d.%d.%d", addr.Scope[0], addr.Scope[1], addr.Scope[2], addr.Scope[3]) + netIntf.Mask = parseMask(addr.Prefix) + break + } } - for _, addr := range addrs { - ipNet, ok := addr.(*net.IPNet) - if !ok { - continue + // Look for IPv6 address + for _, addr := range rawIntf.Addresses { + if addr.Family == 10 { // IPv6 + netIntf.IPv6 = rawIntf.IPv6 + break } + } - // Skip IPv6 addresses - if ipNet.IP.To4() == nil { - intf.IPv6 = ipNet.IP.String() - } else if ipNet.IP.To16() != nil { - intf.IPv4 = ipNet.IP.String() + // Look for default gateway + for _, route := range rawIntf.Routes { + if route.Type == "unicast" && route.Gateway != "" { + netIntf.Gateway = route.Gateway + break } } - nets = append(nets, intf) + nets = append(nets, netIntf) + } + + return nets, nil +} + +func GetDNSConfig() ([]*DNSConfig, error) { + cmd := exec.Command("/usr/bin/resolvectl", "status") + output, err := cmd.Output() + if err != nil { + return nil, err + } + + var dnsConfigs []*DNSConfig + var currentConfig *DNSConfig + + scanner := bufio.NewScanner(strings.NewReader(string(output))) + dnsServerRegex := regexp.MustCompile(`DNS Servers? ([\d.]+)`) + interfaceRegex := regexp.MustCompile(`Link (\d+) \(([^)]+)\)`) + + for scanner.Scan() { + line := scanner.Text() + + if strings.HasPrefix(line, "Global") { + currentConfig = &DNSConfig{Interface: "global"} + dnsConfigs = append(dnsConfigs, currentConfig) + } else if matches := interfaceRegex.FindStringSubmatch(line); len(matches) == 3 { + currentConfig = &DNSConfig{Interface: matches[2]} + dnsConfigs = append(dnsConfigs, currentConfig) + } + + if dnsServerRegex.MatchString(line) { + server := dnsServerRegex.FindStringSubmatch(line)[1] + currentConfig.DNSServers = append(currentConfig.DNSServers, server) + } + } + + if err := scanner.Err(); err != nil { + return nil, err } - return + return dnsConfigs, nil }