diff --git a/.gitignore b/.gitignore index 62c8935..ec4ecb3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.idea/ \ No newline at end of file +.idea/ +test_* \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..db4dfa7 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module gitlab.com/bboehmke/raspi-alpine-builder + +go 1.13 + +require ( + github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46 + github.com/stretchr/testify v1.4.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..eb9f130 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46 h1:rs0kDBt2zF4/CM9rO5/iH+U22jnTygPlqWgX55Ufcxg= +github.com/GehirnInc/crypt v0.0.0-20190301055215-6c0105aabd46/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/network.go b/network.go new file mode 100644 index 0000000..5553f33 --- /dev/null +++ b/network.go @@ -0,0 +1,171 @@ +package alpine_builder + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io/ioutil" + "os" + "regexp" + "strings" +) + +// network configuration +var networkConfig = "/data/etc/network/interfaces" +var networkInterface = "eth0" + +// regular expressions +var reSectionStart = regexp.MustCompile("^(iface|mapping|auto|allow-|source)") +var reIface = regexp.MustCompile("^iface (\\w+) inet (\\w+)") +var reStaticAddress = regexp.MustCompile("^(\\s*)address ([0-9.]+)") +var reStaticNetmask = regexp.MustCompile("^(\\s*)netmask ([0-9.]+)") +var reStaticGateway = regexp.MustCompile("^(\\s*)gateway ([0-9.]+)") + +// NetworkInfo represents the actual network configuration +type NetworkInfo struct { + IsStatic bool + Address string + Netmask string + Gateway string +} + +// GetNetworkInfo from config file +func GetNetworkInfo() (*NetworkInfo, error) { + // load config file + file, err := os.Open(networkConfig) + if err != nil { + return nil, fmt.Errorf("failed to open network config: %w", err) + } + defer file.Close() + + var info *NetworkInfo + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // check for section start + if reSectionStart.MatchString(line) { + infLine := reIface.FindStringSubmatch(line) + if infLine != nil { + // interface found + if infLine[1] == networkInterface { + info = &NetworkInfo{ + IsStatic: infLine[2] == "static", + } + } + } else { + // info set -> stop here + if info != nil { + break + } + } + + } else if info != nil { + // interface config + address := reStaticAddress.FindStringSubmatch(line) + if address != nil { + info.Address = address[2] + } + netmask := reStaticNetmask.FindStringSubmatch(line) + if netmask != nil { + info.Netmask = netmask[2] + } + gateway := reStaticGateway.FindStringSubmatch(line) + if gateway != nil { + info.Gateway = gateway[2] + } + } + } + if info != nil { + return info, nil + } + return nil, errors.New("invalid network config") +} + +// NetworkEnableDHCP configures the network to use DHCP client +func NetworkEnableDHCP() error { + // load config file + file, err := os.Open(networkConfig) + if err != nil { + return fmt.Errorf("failed to open network config: %w", err) + } + defer file.Close() + + var buffer bytes.Buffer + var infFound bool + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // check for section start + if reSectionStart.MatchString(line) { + infLine := reIface.FindStringSubmatch(line) + if infLine != nil { + // interface found + if infLine[1] == networkInterface { + infFound = true + buffer.WriteString(fmt.Sprintf("iface %s inet dhcp\n", networkInterface)) + continue + } + } else { + if infFound { + infFound = false + } + } + buffer.WriteString(line + "\n") + + } else if !infFound { + buffer.WriteString(line + "\n") + } + } + + // write config file + return ioutil.WriteFile(networkConfig, buffer.Bytes(), os.ModePerm) +} + +// NetworkSetStatic IP configuration +func NetworkSetStatic(address, netmask, gateway string) error { + // load config file + file, err := os.Open(networkConfig) + if err != nil { + return fmt.Errorf("failed to open network config: %w", err) + } + defer file.Close() + + var buffer bytes.Buffer + var infFound bool + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // check for section start + if reSectionStart.MatchString(line) { + infLine := reIface.FindStringSubmatch(line) + if infLine != nil { + // interface found + if infLine[1] == networkInterface { + infFound = true + buffer.WriteString(fmt.Sprintf("iface %s inet static\n", networkInterface)) + buffer.WriteString(fmt.Sprintf(" address %s\n", address)) + buffer.WriteString(fmt.Sprintf(" netmask %s\n", netmask)) + buffer.WriteString(fmt.Sprintf(" gateway %s\n", gateway)) + continue + } + } else { + if infFound { + infFound = false + } + } + buffer.WriteString(line + "\n") + + } else if !infFound { + buffer.WriteString(line + "\n") + } + } + + // write config file + return ioutil.WriteFile(networkConfig, buffer.Bytes(), os.ModePerm) +} diff --git a/network_test.go b/network_test.go new file mode 100644 index 0000000..ade6384 --- /dev/null +++ b/network_test.go @@ -0,0 +1,116 @@ +package alpine_builder + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + networkConfig = "test_interfaces" +} + +func TestGetNetworkInfo(t *testing.T) { + ass := assert.New(t) + + // file does not exist + _, err := GetNetworkInfo() + ass.EqualError(err, "failed to open network config: open test_interfaces: no such file or directory") + + // invalid config file + ass.NoError(ioutil.WriteFile(networkConfig, []byte(""), os.ModePerm)) + _, err = GetNetworkInfo() + ass.EqualError(err, "invalid network config") + + // dynamic config + ass.NoError(ioutil.WriteFile(networkConfig, []byte(`auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet dhcp`), os.ModePerm)) + info, err := GetNetworkInfo() + ass.NoError(err) + ass.False(info.IsStatic) + + // static config + ass.NoError(ioutil.WriteFile(networkConfig, []byte(`auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet static + address 1.2.3.4 + netmask 255.255.255.0 + gateway 4.3.2.1`), os.ModePerm)) + info, err = GetNetworkInfo() + ass.NoError(err) + ass.True(info.IsStatic) + ass.Equal("1.2.3.4", info.Address) + ass.Equal("255.255.255.0", info.Netmask) + ass.Equal("4.3.2.1", info.Gateway) + + _ = os.Remove(networkConfig) +} + +func TestNetworkEnableDHCP(t *testing.T) { + ass := assert.New(t) + + // file does not exist + _, err := GetNetworkInfo() + ass.EqualError(err, "failed to open network config: open test_interfaces: no such file or directory") + + ass.NoError(ioutil.WriteFile(networkConfig, []byte(`auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet static + address 1.2.3.4 + netmask 255.255.255.0 + gateway 4.3.2.1`), os.ModePerm)) + + ass.NoError(NetworkEnableDHCP()) + + data, err := ioutil.ReadFile(networkConfig) + ass.NoError(err) + + ass.Equal(`auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet dhcp +`, string(data)) + + _ = os.Remove(networkConfig) +} + +func TestNetworkSetStatic(t *testing.T) { + ass := assert.New(t) + + // file does not exist + _, err := GetNetworkInfo() + ass.EqualError(err, "failed to open network config: open test_interfaces: no such file or directory") + + ass.NoError(ioutil.WriteFile(networkConfig, []byte(`auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet dhcp`), os.ModePerm)) + + ass.NoError(NetworkSetStatic("1.2.3.4", "255.255.255.0", "4.3.2.1")) + + data, err := ioutil.ReadFile(networkConfig) + ass.NoError(err) + + ass.Equal(`auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet static + address 1.2.3.4 + netmask 255.255.255.0 + gateway 4.3.2.1 +`, string(data)) + + _ = os.Remove(networkConfig) +} diff --git a/resources/build.sh b/resources/build.sh index 9a1ed67..1664dba 100755 --- a/resources/build.sh +++ b/resources/build.sh @@ -99,7 +99,7 @@ EOF # prepare network chroot_exec rc-update add networking default -ln -fs /data/etc/interfaces ${ROOTFS_PATH}/etc/network/interfaces +ln -fs /data/etc/network/interfaces ${ROOTFS_PATH}/etc/network/interfaces # run local before network -> local brings up the interface sed -i '/^\tneed/ s/$/ local/' ${ROOTFS_PATH}/etc/init.d/networking @@ -200,8 +200,8 @@ root_pw=\$(mkpasswd -m sha-512 -s "${DEFAULT_ROOT_PASSWORD}") echo "root:\${root_pw}:0:0:::::" > /data/etc/shadow # interface -if [ ! -f /data/etc/interfaces ]; then -cat > /data/etc/interfaces < /data/etc/network/interfaces < fallback to defaults") + } + + crc := binary.LittleEndian.Uint32(data[1020:]) + bla := crc32.ChecksumIEEE(data[:1020]) + if crc != bla { + return defaultData, errors.New("invalid crc -> fallback to defaults") + } + + return data, nil +} + +// saveUbootDat from byte array +func saveUbootDat(data []byte) error { + // update crc + binary.LittleEndian.PutUint32(data[1020:], + crc32.ChecksumIEEE(data[:1020])) + + // remount boot partition - writable + cmd := exec.Command(ubootRemountRW) + err := cmd.Run() + if err != nil { + return fmt.Errorf("failed to remount RW: %w", err) + } + + // update uboot dat file + err = ioutil.WriteFile(ubootFile, data[:1024], os.ModePerm) + if err != nil { + return fmt.Errorf("failed write uboot dat: %w", err) + } + + // remount boot partition - readonly + cmd = exec.Command(ubootRemountRO) + err = cmd.Run() + if err != nil { + return fmt.Errorf("failed to remount RO: %w", err) + } + return nil +} + +// UBootResetCounter to 0 +func UBootResetCounter() error { + data, _ := loadUbootDat() + data[1] = 0 + return saveUbootDat(data) +} + +// UBootActive returns the active partition. A=1, B=2 +func UBootActive() uint8 { + data, _ := loadUbootDat() + return data[2] +} + +// UBootSetActive sets the active partition. A=1, B=2 +func UBootSetActive(active uint8) error { + data, _ := loadUbootDat() + if active == 1 { + data[2] = 1 + } else { + data[2] = 2 + } + return saveUbootDat(data) +} + +// UpdateSystem with the given image +func UpdateSystem(image string) error { + data, _ := loadUbootDat() + rootPart := rootPartitionA + if data[2] == 1 { + rootPart = rootPartitionB + } + + // open image file + inFile, err := os.Open(image) + if err != nil { + log.Fatal(err) + } + defer inFile.Close() + + // decompress image + inDecompress, err := gzip.NewReader(inFile) + if err != nil { + log.Fatal(err) + } + defer inDecompress.Close() + + // open root partition + out, err := os.OpenFile(rootPart, + os.O_WRONLY|os.O_TRUNC|os.O_SYNC, os.ModePerm) + if err != nil { + log.Fatal(err) + } + defer out.Close() + + // write update + _, err = io.Copy(out, inDecompress) + if err != nil { + log.Fatal(err) + } + + // switch active partition + if data[2] == 1 { + return UBootSetActive(2) + } else { + return UBootSetActive(1) + } +} diff --git a/update_test.go b/update_test.go new file mode 100644 index 0000000..19aad20 --- /dev/null +++ b/update_test.go @@ -0,0 +1,179 @@ +package alpine_builder + +import ( + "bytes" + "compress/gzip" + "crypto/rand" + "encoding/binary" + "io" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + ubootFile = "test_uboot" + ubootRemountRW = "true" + ubootRemountRO = "true" + rootPartitionA = "test_rootA" + rootPartitionB = "test_rootB" +} + +func TestLoadUbootDat(t *testing.T) { + ass := assert.New(t) + + // file missing + data, err := loadUbootDat() + ass.EqualError(err, "failed to open file: open test_uboot: no such file or directory") + ass.Equal([]byte{1, 0, 1}, data[:3]) + + // file with invalid data + ass.NoError(ioutil.WriteFile(ubootFile, []byte{}, os.ModePerm)) + + data, err = loadUbootDat() + ass.EqualError(err, "invalid dat file -> fallback to defaults") + ass.Equal([]byte{1, 0, 1}, data[:3]) + + // file with invalid CRC + testData := make([]byte, 1024) + testData[0] = 1 + testData[1] = 2 + testData[2] = 2 + + ass.NoError(ioutil.WriteFile(ubootFile, testData, os.ModePerm)) + data, err = loadUbootDat() + ass.EqualError(err, "invalid crc -> fallback to defaults") + ass.Equal([]byte{1, 0, 1}, data[:3]) + + // file with valid CRC + binary.LittleEndian.PutUint32(testData[1020:], 0x982E8B7A) + + ass.NoError(ioutil.WriteFile(ubootFile, testData, os.ModePerm)) + data, err = loadUbootDat() + ass.NoError(err) + ass.Equal(testData, data) + + _ = os.Remove(ubootFile) +} + +func TestSaveUbootDat(t *testing.T) { + ass := assert.New(t) + + testData := make([]byte, 1024) + testData[0] = 1 + testData[1] = 2 + testData[2] = 2 + ass.NoError(saveUbootDat(testData)) + + binary.LittleEndian.PutUint32(testData[1020:], 0x982E8B7A) + + data, err := ioutil.ReadFile(ubootFile) + ass.NoError(err) + ass.Equal(testData, data) + + _ = os.Remove(ubootFile) +} + +func TestUBootResetCounter(t *testing.T) { + ass := assert.New(t) + + // write test file + testData := make([]byte, 1024) + testData[0] = 1 + testData[1] = 2 + testData[2] = 2 + binary.LittleEndian.PutUint32(testData[1020:], 0x982E8B7A) + ass.NoError(ioutil.WriteFile(ubootFile, testData, os.ModePerm)) + + ass.NoError(UBootResetCounter()) + + data, err := ioutil.ReadFile(ubootFile) + ass.NoError(err) + ass.Zero(data[1]) + + _ = os.Remove(ubootFile) +} + +func TestUBootActive(t *testing.T) { + ass := assert.New(t) + + // write test file + testData := make([]byte, 1024) + testData[0] = 1 + testData[1] = 2 + testData[2] = 2 + binary.LittleEndian.PutUint32(testData[1020:], 0x982E8B7A) + ass.NoError(ioutil.WriteFile(ubootFile, testData, os.ModePerm)) + + ass.Equal(uint8(2), UBootActive()) + + _ = os.Remove(ubootFile) +} + +func TestUBootSetActive(t *testing.T) { + ass := assert.New(t) + + // write test file + testData := make([]byte, 1024) + testData[0] = 1 + testData[1] = 2 + testData[2] = 2 + binary.LittleEndian.PutUint32(testData[1020:], 0x982E8B7A) + ass.NoError(ioutil.WriteFile(ubootFile, testData, os.ModePerm)) + + ass.NoError(UBootSetActive(1)) + + data, err := ioutil.ReadFile(ubootFile) + ass.NoError(err) + ass.Equal(uint8(1), data[2]) + + _ = os.Remove(ubootFile) +} + +func TestUpdateSystem(t *testing.T) { + ass := assert.New(t) + + // test uboot file + testData := make([]byte, 1024) + testData[0] = 1 + testData[1] = 2 + testData[2] = 2 + binary.LittleEndian.PutUint32(testData[1020:], 0x982E8B7A) + ass.NoError(ioutil.WriteFile(ubootFile, testData, os.ModePerm)) + + // generate test image content + size := int64(1024 * 1024 * 5) + testImgData := make([]byte, size) + buffer := bytes.NewBuffer(testImgData) + _, err := io.CopyN(buffer, rand.Reader, size) + ass.NoError(err) + + // write compressed image file + file, err := os.Create("test_image.gz") + ass.NoError(err) + gzipWriter := gzip.NewWriter(file) + _, err = gzipWriter.Write(testImgData) + ass.NoError(err) + ass.NoError(gzipWriter.Close()) + ass.NoError(file.Close()) + + ass.NoError(ioutil.WriteFile(rootPartitionA, nil, os.ModePerm)) + ass.NoError(UpdateSystem("test_image.gz")) + + // check if image was written + data, err := ioutil.ReadFile(rootPartitionA) + ass.NoError(err) + ass.Equal(testImgData, data) + + // check if uboot dat was updated + data, err = ioutil.ReadFile(ubootFile) + ass.NoError(err) + ass.Equal(uint8(1), data[2]) + + _ = os.Remove("test_image.gz") + _ = os.Remove(ubootFile) + _ = os.Remove(rootPartitionA) + _ = os.Remove(rootPartitionB) +}