Repository: pojntfx/go-nbd Branch: main Commit: 2d78be2564f3 Files: 22 Total size: 51.4 KB Directory structure: gitextract_lo09498w/ ├── .github/ │ └── workflows/ │ └── hydrun.yaml ├── .gitignore ├── Hydrunfile ├── LICENSE ├── Makefile ├── README.md ├── cmd/ │ ├── go-nbd-example-client/ │ │ └── main.go │ ├── go-nbd-example-server-file/ │ │ └── main.go │ └── go-nbd-example-server-memory/ │ └── main.go ├── go.mod ├── go.sum └── pkg/ ├── backend/ │ ├── backend.go │ ├── file.go │ └── memory.go ├── client/ │ └── nbd.go ├── ioctl/ │ ├── negotiation_cgo.go │ ├── negotiation_go_amd64.go │ ├── transmission_cgo.go │ └── transmission_go_amd64.go ├── protocol/ │ ├── negotiation.go │ └── transmission.go └── server/ └── nbd.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/hydrun.yaml ================================================ name: hydrun CI on: push: pull_request: schedule: - cron: "0 0 * * 0" jobs: build-linux: runs-on: ${{ matrix.target.runner }} permissions: contents: read strategy: matrix: target: # Tests - id: test src: . os: golang:bookworm flags: -e '-v /tmp/ccache:/root/.cache/go-build' cmd: GOFLAGS="-short" ./Hydrunfile test dst: out/nonexistent runner: ubuntu-latest # We don't vendor the binaries steps: - name: Checkout uses: actions/checkout@v4 - name: Restore ccache uses: actions/cache/restore@v4 with: path: | /tmp/ccache key: cache-ccache-${{ matrix.target.id }} - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Set up hydrun run: | curl -L -o /tmp/hydrun "https://github.com/pojntfx/hydrun/releases/latest/download/hydrun.linux-$(uname -m)" sudo install /tmp/hydrun /usr/local/bin - name: Build with hydrun working-directory: ${{ matrix.target.src }} run: hydrun -o ${{ matrix.target.os }} ${{ matrix.target.flags }} "${{ matrix.target.cmd }}" - name: Fix permissions for output run: sudo chown -R $USER . - name: Save ccache uses: actions/cache/save@v4 with: path: | /tmp/ccache key: cache-ccache-${{ matrix.target.id }} - name: Upload output uses: actions/upload-artifact@v4 with: name: ${{ matrix.target.id }} path: ${{ matrix.target.dst }} publish-linux: runs-on: ubuntu-latest permissions: contents: write needs: build-linux steps: - name: Checkout uses: actions/checkout@v4 - name: Download output uses: actions/download-artifact@v4 with: path: /tmp/out - name: Extract branch name id: extract_branch run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" - name: Publish pre-release to GitHub releases if: ${{ github.ref == 'refs/heads/main' }} uses: softprops/action-gh-release@v2 with: tag_name: release-${{ steps.extract_branch.outputs.branch }} prerelease: true files: | /tmp/out/*/* - name: Publish release to GitHub releases if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@v2 with: prerelease: false files: | /tmp/out/*/* ================================================ FILE: .gitignore ================================================ out ================================================ FILE: Hydrunfile ================================================ #!/bin/bash set -e # Test if [ "$1" = "test" ]; then # Configure Git git config --global --add safe.directory '*' # Generate dependencies make depend # Run tests make test exit 0 fi ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ # Public variables DESTDIR ?= PREFIX ?= /usr/local OUTPUT_DIR ?= out DST ?= # Private variables obj = go-nbd-example-client go-nbd-example-server-file go-nbd-example-server-memory all: $(addprefix build/,$(obj)) # Build build: $(addprefix build/,$(obj)) $(addprefix build/,$(obj)): ifdef DST go build -o $(DST) ./cmd/$(subst build/,,$@) else go build -o $(OUTPUT_DIR)/$(subst build/,,$@) ./cmd/$(subst build/,,$@) endif # Install install: $(addprefix install/,$(obj)) $(addprefix install/,$(obj)): install -D -m 0755 $(OUTPUT_DIR)/$(subst install/,,$@) $(DESTDIR)$(PREFIX)/bin/$(subst install/,,$@) # Uninstall uninstall: $(addprefix uninstall/,$(obj)) $(addprefix uninstall/,$(obj)): rm $(DESTDIR)$(PREFIX)/bin/$(subst uninstall/,,$@) # Run $(addprefix run/,$(obj)): $(subst run/,,$@) $(ARGS) # Test test: go test -timeout 3600s -parallel $(shell nproc) ./... # Benchmark benchmark: go test -timeout 3600s -bench=./... ./... # Clean clean: rm -rf out # Dependencies depend: true ================================================ FILE: README.md ================================================ Project icon # go-nbd Pure Go NBD server and client library.
[![hydrun CI](https://github.com/pojntfx/go-nbd/actions/workflows/hydrun.yaml/badge.svg)](https://github.com/pojntfx/go-nbd/actions/workflows/hydrun.yaml) ![Go Version](https://img.shields.io/badge/go%20version-%3E=1.20-61CFDD.svg) [![Go Reference](https://pkg.go.dev/badge/github.com/pojntfx/go-nbd.svg)](https://pkg.go.dev/github.com/pojntfx/go-nbd) [![Matrix](https://img.shields.io/matrix/go-nbd:matrix.org)](https://matrix.to/#/#go-nbd:matrix.org?via=matrix.org) ## Overview go-nbd is a lean NBD server and client library supporting the baseline protocol. It enables you to: - **Build NBD servers and clients in Go:** Develop Network Block Device servers and clients using the efficient and easy-to-understand Go programming language, without having to fallback to CGo. - **Expose any `io.ReadWriter` as a block device:** Effortlessly turn a file, byte slice, S3 bucket or other `io.ReadWriter` into a fully-fledged block device. - **Bridge with legacy services:** If you need to make your application's dynamic data available to a legacy system, providing a NBD interface can be the perfect solution. ## Installation You can add go-nbd to your Go project by running the following: ```shell $ go get github.com/pojntfx/go-nbd/...@latest ``` ## Tutorial > TL;DR: Define a backend, expose it with a server, connect a block device with the client and setup/mount the filesystem. ### 1. Define a Backend First, define a backend; it should conform to this simple interface: ```go type Backend interface { ReadAt(p []byte, off int64) (n int, err error) WriteAt(p []byte, off int64) (n int, err error) Size() (int64, error) Sync() error } ``` A simple file-based backend could look like this: ```go // server/main.go type FileBackend struct { file *os.File lock sync.RWMutex } func NewFileBackend(file *os.File) *FileBackend { return &FileBackend{file, sync.RWMutex{}} } func (b *FileBackend) ReadAt(p []byte, off int64) (n int, err error) { b.lock.RLock() n, err = b.file.ReadAt(p, off) b.lock.RUnlock() return } func (b *FileBackend) WriteAt(p []byte, off int64) (n int, err error) { b.lock.Lock() n, err = b.file.WriteAt(p, off) b.lock.Unlock() return } func (b *FileBackend) Size() (int64, error) { stat, err := b.file.Stat() if err != nil { return -1, err } return stat.Size(), nil } func (b *FileBackend) Sync() error { return b.file.Sync() } ``` See [pkg/backend](./pkg/backend) for more backend examples. ### 2. Expose the Backend With a Server Next, create the backend and expose it with a server: ```go // server/main.go b := NewFileBackend(f) for { conn, err := l.Accept() if err != nil { continue } go func() { if err := server.Handle( conn, []server.Export{ { Name: *name, Description: *description, Backend: b, }, }, &server.Options{ ReadOnly: *readOnly, MinimumBlockSize: uint32(*minimumBlockSize), PreferredBlockSize: uint32(*preferredBlockSize), MaximumBlockSize: uint32(*maximumBlockSize), }); err != nil { panic(err) } }() } ``` See [cmd/go-nbd-example-server-file/main.go](./cmd/go-nbd-example-server-file/main.go) for the full example. ### 3. Connect to the Server with a Client In a new `main` package, connect to the server by creating a client; note that you'll have to `modprobe nbd` and run the command as `root`: ```go // client/main.go if err := client.Connect(conn, f, &client.Options{ ExportName: *name, BlockSize: uint32(*blockSize), }); err != nil { panic(err) } ``` See [cmd/go-nbd-example-client/main.go](./cmd/go-nbd-example-client/main.go) for the full example. ### 4. Setup and Mount the Filesystem Lastly, create a filesystem on the block device and mount it: ```shell $ sudo mkfs.ext4 /dev/nbd0 $ sudo mkdir -p /mnt $ sudo mount -t ext4 /dev/nbd0 /mnt ``` You should now be able to use the mounted filesystem by navigating to `/mnt`. 🚀 That's it! We can't wait to see what you're going to build with go-nbd. ## Examples To make getting started with go-nbd easier, take a look at the following examples: - [NBD File Server](./cmd/go-nbd-example-server-file/main.go) - [NBD Memory Server](./cmd/go-nbd-example-server-memory/main.go) - [NBD Client](./cmd/go-nbd-example-client/main.go) ## Acknowledgements - [abligh/gonbdserver](https://github.com/abligh/gonbdserver/) provided the initial inspiration for this project. - [NetworkBlockDevice/nbd](https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md) provided the NBD protocol documentation. ## Contributing To contribute, please use the [GitHub flow](https://guides.github.com/introduction/flow/) and follow our [Code of Conduct](./CODE_OF_CONDUCT.md). To build and start a development version of one of the examples locally, run the following: ```shell $ git clone https://github.com/pojntfx/go-nbd.git $ cd go-nbd $ mkdir -p out && rm -f out/disk.img && truncate -s 10G out/disk.img && go run ./cmd/go-nbd-example-server-file --file out/disk.img $ go run ./cmd/go-nbd-example-server-memory # With the C NBD client $ sudo umount ~/Downloads/mnt; sudo nbd-client -d /dev/nbd1 && echo 'NBD starting' | sudo tee /dev/kmsg && sudo nbd-client -N default localhost 10809 /dev/nbd1 # With the Go NBD client $ sudo umount ~/Downloads/mnt; go build -o /tmp/go-nbd-example-client ./cmd/go-nbd-example-client/ && sudo /tmp/go-nbd-example-client --file /dev/nbd1 $ sudo mkfs.ext4 /dev/nbd1 $ sync -f ~/Downloads/mnt; sudo umount ~/Downloads/mnt; sudo rm -rf ~/Downloads/mnt && sudo mkdir -p ~/Downloads/mnt && sudo mount -t ext4 /dev/nbd1 ~/Downloads/mnt && sudo chown -R "${USER}" ~/Downloads/mnt ``` Have any questions or need help? Chat with us [on Matrix](https://matrix.to/#/#go-nbd:matrix.org?via=matrix.org)! ## License go-nbd (c) 2024 Felicitas Pojtinger and contributors SPDX-License-Identifier: Apache-2.0 ================================================ FILE: cmd/go-nbd-example-client/main.go ================================================ package main import ( "encoding/json" "flag" "log" "net" "os" "os/signal" "github.com/pojntfx/go-nbd/pkg/client" ) func main() { file := flag.String("file", "/dev/nbd0", "Path to device file to create") raddr := flag.String("raddr", "127.0.0.1:10809", "Remote address") network := flag.String("network", "tcp", "Remote network (e.g. `tcp` or `unix`)") name := flag.String("name", "default", "Export name") list := flag.Bool("list", false, "List the exports and exit") blockSize := flag.Uint("block-size", 0, "Block size to use; 0 uses the server's preferred block size") flag.Parse() conn, err := net.Dial(*network, *raddr) if err != nil { panic(err) } defer conn.Close() log.Println("Connected to", conn.RemoteAddr()) if *list { exports, err := client.List(conn) if err != nil { panic(err) } if err := json.NewEncoder(os.Stdout).Encode(exports); err != nil { panic(err) } return } f, err := os.Open(*file) if err != nil { panic(err) } defer f.Close() sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt) go func() { for range sigCh { if err := client.Disconnect(f); err != nil { panic(err) } os.Exit(0) } }() if err := client.Connect(conn, f, &client.Options{ ExportName: *name, BlockSize: uint32(*blockSize), }); err != nil { panic(err) } } ================================================ FILE: cmd/go-nbd-example-server-file/main.go ================================================ package main import ( "flag" "log" "net" "os" "github.com/pojntfx/go-nbd/pkg/backend" "github.com/pojntfx/go-nbd/pkg/client" "github.com/pojntfx/go-nbd/pkg/server" ) func main() { file := flag.String("file", "disk.img", "Path to file to expose") laddr := flag.String("laddr", ":10809", "Listen address") network := flag.String("network", "tcp", "Listen network (e.g. `tcp` or `unix`)") name := flag.String("name", "default", "Export name") description := flag.String("description", "The default export", "Export description") readOnly := flag.Bool("read-only", false, "Whether the export should be read-only") minimumBlockSize := flag.Uint("minimum-block-size", 1, "Minimum block size") preferredBlockSize := flag.Uint("preferred-block-size", client.MaximumBlockSize, "Preferred block size") maximumBlockSize := flag.Uint("maximum-block-size", 0xffffffff, "Maximum block size") multiConn := flag.Bool("multi-conn", true, "Whether to advertise support for multiple simultaneous connections") flag.Parse() l, err := net.Listen(*network, *laddr) if err != nil { panic(err) } defer l.Close() log.Println("Listening on", l.Addr()) var f *os.File if *readOnly { f, err = os.OpenFile(*file, os.O_RDONLY, 0644) if err != nil { panic(err) } } else { f, err = os.OpenFile(*file, os.O_RDWR, 0644) if err != nil { panic(err) } } defer f.Close() b := backend.NewFileBackend(f) clients := 0 for { conn, err := l.Accept() if err != nil { log.Println("Could not accept connection, continuing:", err) continue } clients++ log.Printf("%v clients connected", clients) go func() { defer func() { _ = conn.Close() clients-- if err := recover(); err != nil { log.Printf("Client disconnected with error: %v", err) } log.Printf("%v clients connected", clients) }() if err := server.Handle( conn, []*server.Export{ { Name: *name, Description: *description, Backend: b, }, }, &server.Options{ ReadOnly: *readOnly, MinimumBlockSize: uint32(*minimumBlockSize), PreferredBlockSize: uint32(*preferredBlockSize), MaximumBlockSize: uint32(*maximumBlockSize), SupportsMultiConn: *multiConn, }); err != nil { panic(err) } }() } } ================================================ FILE: cmd/go-nbd-example-server-memory/main.go ================================================ package main import ( "flag" "log" "net" "github.com/pojntfx/go-nbd/pkg/backend" "github.com/pojntfx/go-nbd/pkg/client" "github.com/pojntfx/go-nbd/pkg/server" ) func main() { size := flag.Int64("size", 1073741824, "Size of the memory region to expose") laddr := flag.String("laddr", ":10809", "Listen address") network := flag.String("network", "tcp", "Listen network (e.g. `tcp` or `unix`)") name := flag.String("name", "default", "Export name") description := flag.String("description", "The default export", "Export description") readOnly := flag.Bool("read-only", false, "Whether the export should be read-only") minimumBlockSize := flag.Uint("minimum-block-size", 1, "Minimum block size") preferredBlockSize := flag.Uint("preferred-block-size", client.MaximumBlockSize, "Preferred block size") maximumBlockSize := flag.Uint("maximum-block-size", 0xffffffff, "Maximum block size") multiConn := flag.Bool("multi-conn", true, "Whether to advertise support for multiple simultaneous connections") flag.Parse() l, err := net.Listen(*network, *laddr) if err != nil { panic(err) } defer l.Close() log.Println("Listening on", l.Addr()) b := backend.NewMemoryBackend(make([]byte, *size)) clients := 0 for { conn, err := l.Accept() if err != nil { log.Println("Could not accept connection, continuing:", err) continue } clients++ log.Printf("%v clients connected", clients) go func() { defer func() { _ = conn.Close() clients-- if err := recover(); err != nil { log.Printf("Client disconnected with error: %v", err) } log.Printf("%v clients connected", clients) }() if err := server.Handle( conn, []*server.Export{ { Name: *name, Description: *description, Backend: b, }, }, &server.Options{ ReadOnly: *readOnly, MinimumBlockSize: uint32(*minimumBlockSize), PreferredBlockSize: uint32(*preferredBlockSize), MaximumBlockSize: uint32(*maximumBlockSize), SupportsMultiConn: *multiConn, }); err != nil { panic(err) } }() } } ================================================ FILE: go.mod ================================================ module github.com/pojntfx/go-nbd go 1.20 require github.com/pilebones/go-udev v0.9.0 ================================================ FILE: go.sum ================================================ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 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/pilebones/go-udev v0.9.0 h1:N1uEO/SxUwtIctc0WLU0t69JeBxIYEYnj8lT/Nabl9Q= github.com/pilebones/go-udev v0.9.0/go.mod h1:T2eI2tUSK0hA2WS5QLjXJUfQkluZQu+18Cqvem3CaXI= ================================================ FILE: pkg/backend/backend.go ================================================ package backend import "io" type Backend interface { io.ReaderAt io.WriterAt Size() (int64, error) Sync() error } ================================================ FILE: pkg/backend/file.go ================================================ package backend import ( "io" "os" "sync" ) type FileBackend struct { file *os.File lock sync.RWMutex } func NewFileBackend(file *os.File) *FileBackend { return &FileBackend{file, sync.RWMutex{}} } func (b *FileBackend) ReadAt(p []byte, off int64) (n int, err error) { b.lock.RLock() n, err = b.file.ReadAt(p, off) b.lock.RUnlock() return } func (b *FileBackend) WriteAt(p []byte, off int64) (n int, err error) { b.lock.Lock() n, err = b.file.WriteAt(p, off) b.lock.Unlock() return } func (b *FileBackend) Size() (int64, error) { size, err := b.file.Seek(0, io.SeekEnd) if err != nil { return -1, err } return size, nil } func (b *FileBackend) Sync() error { return b.file.Sync() } ================================================ FILE: pkg/backend/memory.go ================================================ package backend import ( "io" "sync" ) type MemoryBackend struct { memory []byte lock sync.Mutex } func NewMemoryBackend(memory []byte) *MemoryBackend { return &MemoryBackend{memory, sync.Mutex{}} } func (b *MemoryBackend) ReadAt(p []byte, off int64) (n int, err error) { b.lock.Lock() if off >= int64(len(b.memory)) { return 0, io.EOF } n = copy(p, b.memory[off:off+int64(len(p))]) b.lock.Unlock() return } func (b *MemoryBackend) WriteAt(p []byte, off int64) (n int, err error) { b.lock.Lock() if off >= int64(len(b.memory)) { return 0, io.EOF } n = copy(b.memory[off:off+int64(len(p))], p) if n < len(p) { return n, io.ErrShortWrite } b.lock.Unlock() return } func (b *MemoryBackend) Size() (int64, error) { return int64(len(b.memory)), nil } func (b *MemoryBackend) Sync() error { return nil } ================================================ FILE: pkg/client/nbd.go ================================================ package client import ( "bytes" "encoding/binary" "errors" "io" "net" "os" "path/filepath" "strconv" "strings" "syscall" "time" "github.com/pilebones/go-udev/netlink" "github.com/pojntfx/go-nbd/pkg/ioctl" "github.com/pojntfx/go-nbd/pkg/protocol" "github.com/pojntfx/go-nbd/pkg/server" ) const ( MinimumBlockSize = 512 // This is the minimum value that works in practice, else the client stops with "invalid argument" MaximumBlockSize = 4096 // This is the maximum value that works in practice, else the client stops with "invalid argument" ) var ( ErrUnsupportedNetwork = errors.New("unsupported network") ErrUnknownReply = errors.New("unknown reply") ErrUnknownInfo = errors.New("unknown info") ErrUnknownErr = errors.New("unknown error") ErrUnsupportedServerBlockSize = errors.New("server proposed unsupported block size") ErrMinimumBlockSize = errors.New("block size below mimimum requested") ErrMaximumBlockSize = errors.New("block size above maximum requested") ErrBlockSizeNotPowerOfTwo = errors.New("block size is not a power of 2") ) type Options struct { ExportName string BlockSize uint32 OnConnected func() ReadyCheckUdev bool ReadyCheckPollInterval time.Duration Timeout int } func negotiateNewstyle(conn net.Conn) error { var newstyleHeader protocol.NegotiationNewstyleHeader if err := binary.Read(conn, binary.BigEndian, &newstyleHeader); err != nil { return err } if newstyleHeader.OldstyleMagic != protocol.NEGOTIATION_MAGIC_OLDSTYLE { return server.ErrInvalidMagic } if newstyleHeader.OptionMagic != protocol.NEGOTIATION_MAGIC_OPTION { return server.ErrInvalidMagic } if _, err := conn.Write(make([]byte, 4)); err != nil { // Send client flags (uint32) return err } return nil } func Connect(conn net.Conn, device *os.File, options *Options) error { if options == nil { options = &Options{} } if options.ExportName == "" { options.ExportName = "default" } if !options.ReadyCheckUdev && options.ReadyCheckPollInterval <= 0 { options.ReadyCheckPollInterval = time.Millisecond } var cfd uintptr switch c := conn.(type) { case *net.TCPConn: file, err := c.File() if err != nil { return err } cfd = uintptr(file.Fd()) case *net.UnixConn: file, err := c.File() if err != nil { return err } cfd = uintptr(file.Fd()) default: return ErrUnsupportedNetwork } fatal := make(chan error) if options.OnConnected != nil { if options.ReadyCheckUdev { udevConn := new(netlink.UEventConn) if err := udevConn.Connect(netlink.UdevEvent); err != nil { return err } defer udevConn.Close() var ( udevReadyCh = make(chan netlink.UEvent) udevErrCh = make(chan error) udevQuit = udevConn.Monitor(udevReadyCh, udevErrCh, &netlink.RuleDefinitions{ Rules: []netlink.RuleDefinition{ { Env: map[string]string{ "DEVNAME": device.Name(), }, }, }, }) ) defer close(udevQuit) go func() { select { case <-udevReadyCh: close(udevQuit) options.OnConnected() return case err := <-udevErrCh: fatal <- err return } }() } else { go func() { sizeFile, err := os.Open(filepath.Join("/sys", "block", filepath.Base(device.Name()), "size")) if err != nil { fatal <- err return } defer sizeFile.Close() for { if _, err := sizeFile.Seek(0, io.SeekStart); err != nil { fatal <- err return } rsize, err := io.ReadAll(sizeFile) if err != nil { fatal <- err return } size, err := strconv.ParseInt(strings.TrimSpace(string(rsize)), 10, 64) if err != nil { fatal <- err return } if size > 0 { options.OnConnected() return } time.Sleep(options.ReadyCheckPollInterval) } }() } } if _, _, err := syscall.Syscall( syscall.SYS_IOCTL, device.Fd(), ioctl.NEGOTIATION_IOCTL_SET_SOCK, uintptr(cfd), ); err != 0 { return err } if err := negotiateNewstyle(conn); err != nil { return err } if err := binary.Write(conn, binary.BigEndian, protocol.NegotiationOptionHeader{ OptionMagic: protocol.NEGOTIATION_MAGIC_OPTION, ID: protocol.NEGOTIATION_ID_OPTION_GO, Length: 0, }); err != nil { return err } exportName := []byte(options.ExportName) if err := binary.Write(conn, binary.BigEndian, uint32(len(exportName))); err != nil { return err } if _, err := conn.Write([]byte(exportName)); err != nil { return err } if err := binary.Write(conn, binary.BigEndian, uint16(0)); err != nil { // Send information request count (uint16) return err } size := uint64(0) chosenBlockSize := uint32(1) n: for { var replyHeader protocol.NegotiationReplyHeader if err := binary.Read(conn, binary.BigEndian, &replyHeader); err != nil { return err } if replyHeader.ReplyMagic != protocol.NEGOTIATION_MAGIC_REPLY { return server.ErrInvalidMagic } switch replyHeader.Type { case protocol.NEGOTIATION_TYPE_REPLY_INFO: infoRaw := make([]byte, replyHeader.Length) if _, err := io.ReadFull(conn, infoRaw); err != nil { return err } var infoType uint16 if err := binary.Read(bytes.NewBuffer(infoRaw), binary.BigEndian, &infoType); err != nil { return err } switch infoType { case protocol.NEGOTIATION_TYPE_INFO_EXPORT: var info protocol.NegotiationReplyInfo if err := binary.Read(bytes.NewBuffer(infoRaw), binary.BigEndian, &info); err != nil { return err } size = info.Size case protocol.NEGOTIATION_TYPE_INFO_NAME: // Discard export name case protocol.NEGOTIATION_TYPE_INFO_DESCRIPTION: // Discard export description case protocol.NEGOTIATION_TYPE_INFO_BLOCKSIZE: var info protocol.NegotiationReplyBlockSize if err := binary.Read(bytes.NewBuffer(infoRaw), binary.BigEndian, &info); err != nil { return err } if options.BlockSize == 0 { chosenBlockSize = info.PreferredBlockSize } else if options.BlockSize >= info.MinimumBlockSize && options.BlockSize <= info.MaximumBlockSize { chosenBlockSize = options.BlockSize } else { return ErrUnsupportedServerBlockSize } if chosenBlockSize > MaximumBlockSize { return ErrMaximumBlockSize } else if chosenBlockSize < MinimumBlockSize { return ErrMinimumBlockSize } if !((chosenBlockSize > 0) && ((chosenBlockSize & (chosenBlockSize - 1)) == 0)) { return ErrBlockSizeNotPowerOfTwo } default: return ErrUnknownInfo } case protocol.NEGOTIATION_TYPE_REPLY_ACK: break n case protocol.NEGOTIATION_TYPE_REPLY_ERR_UNKNOWN: return ErrUnknownErr default: return ErrUnknownReply } } if _, _, err := syscall.Syscall( syscall.SYS_IOCTL, device.Fd(), ioctl.NEGOTIATION_IOCTL_SET_BLOCKSIZE, uintptr(chosenBlockSize), ); err != 0 { return err } if _, _, err := syscall.Syscall( syscall.SYS_IOCTL, device.Fd(), ioctl.NEGOTIATION_IOCTL_SET_SIZE_BLOCKS, uintptr(size/uint64(chosenBlockSize)), ); err != 0 { return err } if _, _, err := syscall.Syscall( syscall.SYS_IOCTL, device.Fd(), ioctl.NEGOTIATION_IOCTL_SET_TIMEOUT, uintptr(options.Timeout), ); err != 0 { return err } go func() { defer func() { close(fatal) }() if _, _, err := syscall.Syscall( syscall.SYS_IOCTL, device.Fd(), ioctl.NEGOTIATION_IOCTL_DO_IT, 0, ); err != 0 { fatal <- err return } }() return <-fatal } func Disconnect(device *os.File) error { if _, _, err := syscall.Syscall( syscall.SYS_IOCTL, device.Fd(), ioctl.TRANSMISSION_IOCTL_CLEAR_QUE, 0, ); err != 0 { return err } if _, _, err := syscall.Syscall( syscall.SYS_IOCTL, device.Fd(), ioctl.TRANSMISSION_IOCTL_DISCONNECT, 0, ); err != 0 { return err } if _, _, err := syscall.Syscall( syscall.SYS_IOCTL, device.Fd(), ioctl.TRANSMISSION_IOCTL_CLEAR_SOCK, 0, ); err != 0 { return err } return nil } func List(conn net.Conn) ([]string, error) { if err := negotiateNewstyle(conn); err != nil { return []string{}, err } if err := binary.Write(conn, binary.BigEndian, protocol.NegotiationOptionHeader{ OptionMagic: protocol.NEGOTIATION_MAGIC_OPTION, ID: protocol.NEGOTIATION_ID_OPTION_LIST, Length: 0, }); err != nil { return []string{}, err } var replyHeader protocol.NegotiationReplyHeader if err := binary.Read(conn, binary.BigEndian, &replyHeader); err != nil { return []string{}, err } if replyHeader.ReplyMagic != protocol.NEGOTIATION_MAGIC_REPLY { return []string{}, server.ErrInvalidMagic } infoRaw := make([]byte, replyHeader.Length) if _, err := io.ReadFull(conn, infoRaw); err != nil { return []string{}, err } info := bytes.NewBuffer(infoRaw) exportNames := []string{} for { var exportNameLength uint32 if err := binary.Read(info, binary.BigEndian, &exportNameLength); err != nil { if errors.Is(err, io.EOF) { break } return []string{}, err } exportName := make([]byte, exportNameLength) if _, err := io.ReadFull(info, exportName); err != nil { return []string{}, err } exportNames = append(exportNames, string(exportName)) } if err := binary.Write(conn, binary.BigEndian, protocol.NegotiationOptionHeader{ OptionMagic: protocol.NEGOTIATION_MAGIC_OPTION, ID: protocol.NEGOTIATION_ID_OPTION_ABORT, Length: 0, }); err != nil { return []string{}, err } return exportNames, nil } ================================================ FILE: pkg/ioctl/negotiation_cgo.go ================================================ //go:build linux && cgo package ioctl /* #include #include */ import "C" const ( NEGOTIATION_IOCTL_SET_SOCK = C.NBD_SET_SOCK NEGOTIATION_IOCTL_SET_BLOCKSIZE = C.NBD_SET_BLKSIZE NEGOTIATION_IOCTL_SET_SIZE_BLOCKS = C.NBD_SET_SIZE_BLOCKS NEGOTIATION_IOCTL_DO_IT = C.NBD_DO_IT NEGOTIATION_IOCTL_SET_TIMEOUT = C.NBD_SET_TIMEOUT ) ================================================ FILE: pkg/ioctl/negotiation_go_amd64.go ================================================ //go:build linux && !cgo && amd64 package ioctl // See /usr/include/linux/nbd.h const ( NEGOTIATION_IOCTL_SET_SOCK = 43776 NEGOTIATION_IOCTL_SET_BLOCKSIZE = 43777 NEGOTIATION_IOCTL_SET_SIZE_BLOCKS = 43783 NEGOTIATION_IOCTL_DO_IT = 43779 NEGOTIATION_IOCTL_SET_TIMEOUT = 43785 ) ================================================ FILE: pkg/ioctl/transmission_cgo.go ================================================ //go:build linux && cgo package ioctl /* #include #include */ import "C" const ( TRANSMISSION_IOCTL_DISCONNECT = C.NBD_DISCONNECT TRANSMISSION_IOCTL_CLEAR_SOCK = C.NBD_CLEAR_SOCK TRANSMISSION_IOCTL_CLEAR_QUE = C.NBD_CLEAR_QUE ) ================================================ FILE: pkg/ioctl/transmission_go_amd64.go ================================================ //go:build linux && !cgo && amd64 package ioctl const ( TRANSMISSION_IOCTL_DISCONNECT = 43784 TRANSMISSION_IOCTL_CLEAR_SOCK = 43780 TRANSMISSION_IOCTL_CLEAR_QUE = 43781 ) ================================================ FILE: pkg/protocol/negotiation.go ================================================ package protocol // See https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md and https://github.com/abligh/gonbdserver/ const ( NEGOTIATION_MAGIC_OLDSTYLE = uint64(0x4e42444d41474943) NEGOTIATION_MAGIC_OPTION = uint64(0x49484156454F5054) NEGOTIATION_MAGIC_REPLY = uint64(0x3e889045565a9) NEGOTIATION_HANDSHAKE_FLAG_FIXED_NEWSTYLE = uint16(1 << 0) NEGOTIATION_ID_OPTION_ABORT = uint32(2) NEGOTIATION_ID_OPTION_LIST = uint32(3) NEGOTIATION_ID_OPTION_INFO = uint32(6) NEGOTIATION_ID_OPTION_GO = uint32(7) NEGOTIATION_TYPE_REPLY_ACK = uint32(1) NEGOTIATION_TYPE_REPLY_SERVER = uint32(2) NEGOTIATION_TYPE_REPLY_INFO = uint32(3) NEGOTIATION_TYPE_REPLY_ERR_UNSUPPORTED = uint32(1 | uint32(1<<31)) NEGOTIATION_TYPE_REPLY_ERR_UNKNOWN = uint32(6 | uint32(1<<31)) NEGOTIATION_TYPE_INFO_EXPORT = uint16(0) NEGOTIATION_TYPE_INFO_NAME = uint16(1) NEGOTIATION_TYPE_INFO_DESCRIPTION = uint16(2) NEGOTIATION_TYPE_INFO_BLOCKSIZE = uint16(3) NEGOTIATION_REPLY_FLAGS_HAS_FLAGS = uint16((1 << 0)) NEGOTIATION_REPLY_FLAGS_CAN_MULTI_CONN = uint16((1 << 8)) ) type NegotiationNewstyleHeader struct { OldstyleMagic uint64 OptionMagic uint64 HandshakeFlags uint16 } type NegotiationOptionHeader struct { OptionMagic uint64 ID uint32 Length uint32 } type NegotiationReplyHeader struct { ReplyMagic uint64 ID uint32 Type uint32 Length uint32 } type NegotiationReplyInfo struct { Type uint16 Size uint64 TransmissionFlags uint16 } type NegotiationReplyNameHeader struct { Type uint16 } type NegotiationReplyDescriptionHeader NegotiationReplyNameHeader type NegotiationReplyBlockSize struct { Type uint16 MinimumBlockSize uint32 PreferredBlockSize uint32 MaximumBlockSize uint32 } ================================================ FILE: pkg/protocol/transmission.go ================================================ package protocol const ( TRANSMISSION_MAGIC_REQUEST = uint32(0x25609513) TRANSMISSION_MAGIC_REPLY = uint32(0x67446698) TRANSMISSION_TYPE_REQUEST_READ = uint16(0) TRANSMISSION_TYPE_REQUEST_WRITE = uint16(1) TRANSMISSION_TYPE_REQUEST_DISC = uint16(2) TRANSMISSION_ERROR_EPERM = uint32(1) TRANSMISSION_ERROR_EINVAL = uint32(22) ) type TransmissionRequestHeader struct { RequestMagic uint32 CommandFlags uint16 Type uint16 Handle uint64 Offset uint64 Length uint32 } type TransmissionReplyHeader struct { ReplyMagic uint32 Error uint32 Handle uint64 } ================================================ FILE: pkg/server/nbd.go ================================================ package server import ( "bytes" "encoding/binary" "errors" "io" "net" "github.com/pojntfx/go-nbd/pkg/backend" "github.com/pojntfx/go-nbd/pkg/protocol" ) var ( ErrInvalidMagic = errors.New("invalid magic") ErrInvalidBlocksize = errors.New("invalid blocksize") ) const ( defaultMaximumRequestSize = 32 * 1024 * 1024 // Support for a 32M maximum packet size is expected: https://sourceforge.net/p/nbd/mailman/message/35081223/ ) type Export struct { Name string Description string Backend backend.Backend } type Options struct { ReadOnly bool MinimumBlockSize uint32 PreferredBlockSize uint32 MaximumBlockSize uint32 MaximumRequestSize int SupportsMultiConn bool } func Handle(conn net.Conn, exports []*Export, options *Options) error { if options == nil { options = &Options{ ReadOnly: false, SupportsMultiConn: true, } } if options.MinimumBlockSize == 0 { options.MinimumBlockSize = 1 } if options.PreferredBlockSize == 0 { options.PreferredBlockSize = 4096 } if options.MaximumBlockSize == 0 { options.MaximumBlockSize = defaultMaximumRequestSize } if options.MaximumRequestSize == 0 { options.MaximumRequestSize = defaultMaximumRequestSize } // Negotiation if err := binary.Write(conn, binary.BigEndian, protocol.NegotiationNewstyleHeader{ OldstyleMagic: protocol.NEGOTIATION_MAGIC_OLDSTYLE, OptionMagic: protocol.NEGOTIATION_MAGIC_OPTION, HandshakeFlags: protocol.NEGOTIATION_HANDSHAKE_FLAG_FIXED_NEWSTYLE, }); err != nil { return err } _, err := io.CopyN(io.Discard, conn, 4) // Discard client flags (uint32) if err != nil { return err } var export *Export n: for { var optionHeader protocol.NegotiationOptionHeader if err := binary.Read(conn, binary.BigEndian, &optionHeader); err != nil { return err } if optionHeader.OptionMagic != protocol.NEGOTIATION_MAGIC_OPTION { return ErrInvalidMagic } switch optionHeader.ID { case protocol.NEGOTIATION_ID_OPTION_INFO, protocol.NEGOTIATION_ID_OPTION_GO: var exportNameLength uint32 if err := binary.Read(conn, binary.BigEndian, &exportNameLength); err != nil { return err } exportName := make([]byte, exportNameLength) if _, err := io.ReadFull(conn, exportName); err != nil { return err } for _, candidate := range exports { if candidate.Name == string(exportName) { export = candidate break } } if export == nil { if length := int64(optionHeader.Length) - 4 - int64(exportNameLength); length > 0 { // Discard the option's data, minus the export name length and export name we've already read _, err := io.CopyN(io.Discard, conn, length) if err != nil { return err } } if err := binary.Write(conn, binary.BigEndian, protocol.NegotiationReplyHeader{ ReplyMagic: protocol.NEGOTIATION_MAGIC_REPLY, ID: optionHeader.ID, Type: protocol.NEGOTIATION_TYPE_REPLY_ERR_UNKNOWN, Length: 0, }); err != nil { return err } break } size, err := export.Backend.Size() if err != nil { return err } { var informationRequestCount uint16 if err := binary.Read(conn, binary.BigEndian, &informationRequestCount); err != nil { return err } _, err := io.CopyN(io.Discard, conn, 2*int64(informationRequestCount)) // Discard information requests (uint16s) if err != nil { return err } } { transmissionFlags := uint16(0) if options.SupportsMultiConn { transmissionFlags = protocol.NEGOTIATION_REPLY_FLAGS_HAS_FLAGS | protocol.NEGOTIATION_REPLY_FLAGS_CAN_MULTI_CONN } info := &bytes.Buffer{} if err := binary.Write(info, binary.BigEndian, protocol.NegotiationReplyInfo{ Type: protocol.NEGOTIATION_TYPE_INFO_EXPORT, Size: uint64(size), TransmissionFlags: transmissionFlags, }); err != nil { return err } if err := binary.Write(conn, binary.BigEndian, protocol.NegotiationReplyHeader{ ReplyMagic: protocol.NEGOTIATION_MAGIC_REPLY, ID: optionHeader.ID, Type: protocol.NEGOTIATION_TYPE_REPLY_INFO, Length: uint32(info.Len()), }); err != nil { return err } if _, err := io.Copy(conn, info); err != nil { return err } } { info := &bytes.Buffer{} if err := binary.Write(info, binary.BigEndian, protocol.NegotiationReplyNameHeader{ Type: protocol.NEGOTIATION_TYPE_INFO_NAME, }); err != nil { return err } if _, err := info.Write([]byte(exportName)); err != nil { return err } if err := binary.Write(conn, binary.BigEndian, protocol.NegotiationReplyHeader{ ReplyMagic: protocol.NEGOTIATION_MAGIC_REPLY, ID: optionHeader.ID, Type: protocol.NEGOTIATION_TYPE_REPLY_INFO, Length: uint32(info.Len()), }); err != nil { return err } if _, err := io.Copy(conn, info); err != nil { return err } } { info := &bytes.Buffer{} if err := binary.Write(info, binary.BigEndian, protocol.NegotiationReplyDescriptionHeader{ Type: protocol.NEGOTIATION_TYPE_INFO_DESCRIPTION, }); err != nil { return err } if err := binary.Write(info, binary.BigEndian, []byte(export.Description)); err != nil { return err } if err := binary.Write(conn, binary.BigEndian, protocol.NegotiationReplyHeader{ ReplyMagic: protocol.NEGOTIATION_MAGIC_REPLY, ID: optionHeader.ID, Type: protocol.NEGOTIATION_TYPE_REPLY_INFO, Length: uint32(info.Len()), }); err != nil { return err } if _, err := io.Copy(conn, info); err != nil { return err } } { info := &bytes.Buffer{} if err := binary.Write(info, binary.BigEndian, protocol.NegotiationReplyBlockSize{ Type: protocol.NEGOTIATION_TYPE_INFO_BLOCKSIZE, MinimumBlockSize: options.MinimumBlockSize, PreferredBlockSize: options.PreferredBlockSize, MaximumBlockSize: options.MaximumBlockSize, }); err != nil { return err } if err := binary.Write(conn, binary.BigEndian, protocol.NegotiationReplyHeader{ ReplyMagic: protocol.NEGOTIATION_MAGIC_REPLY, ID: optionHeader.ID, Type: protocol.NEGOTIATION_TYPE_REPLY_INFO, Length: uint32(info.Len()), }); err != nil { return err } if _, err := io.Copy(conn, info); err != nil { return err } } if err := binary.Write(conn, binary.BigEndian, protocol.NegotiationReplyHeader{ ReplyMagic: protocol.NEGOTIATION_MAGIC_REPLY, ID: optionHeader.ID, Type: protocol.NEGOTIATION_TYPE_REPLY_ACK, Length: 0, }); err != nil { return err } if optionHeader.ID == protocol.NEGOTIATION_ID_OPTION_GO { break n } case protocol.NEGOTIATION_ID_OPTION_ABORT: if err := binary.Write(conn, binary.BigEndian, protocol.NegotiationReplyHeader{ ReplyMagic: protocol.NEGOTIATION_MAGIC_REPLY, ID: optionHeader.ID, Type: protocol.NEGOTIATION_TYPE_REPLY_ACK, Length: 0, }); err != nil { return err } return nil case protocol.NEGOTIATION_ID_OPTION_LIST: { info := &bytes.Buffer{} for _, export := range exports { exportName := []byte(export.Name) if err := binary.Write(info, binary.BigEndian, uint32(len(exportName))); err != nil { return err } if err := binary.Write(info, binary.BigEndian, exportName); err != nil { return err } } if err := binary.Write(conn, binary.BigEndian, protocol.NegotiationReplyHeader{ ReplyMagic: protocol.NEGOTIATION_MAGIC_REPLY, ID: optionHeader.ID, Type: protocol.NEGOTIATION_TYPE_REPLY_SERVER, Length: uint32(info.Len()), }); err != nil { return err } if _, err := io.Copy(conn, info); err != nil { return err } } if err := binary.Write(conn, binary.BigEndian, protocol.NegotiationReplyHeader{ ReplyMagic: protocol.NEGOTIATION_MAGIC_REPLY, ID: optionHeader.ID, Type: protocol.NEGOTIATION_TYPE_REPLY_ACK, Length: 0, }); err != nil { return err } default: _, err := io.CopyN(io.Discard, conn, int64(optionHeader.Length)) // Discard the unknown option's data if err != nil { return err } if err := binary.Write(conn, binary.BigEndian, protocol.NegotiationReplyHeader{ ReplyMagic: protocol.NEGOTIATION_MAGIC_REPLY, ID: optionHeader.ID, Type: protocol.NEGOTIATION_TYPE_REPLY_ERR_UNSUPPORTED, Length: 0, }); err != nil { return err } } } // Transmission b := []byte{} for { var requestHeader protocol.TransmissionRequestHeader if err := binary.Read(conn, binary.BigEndian, &requestHeader); err != nil { return err } if requestHeader.RequestMagic != protocol.TRANSMISSION_MAGIC_REQUEST { return ErrInvalidMagic } length := requestHeader.Length if length > defaultMaximumRequestSize { return ErrInvalidBlocksize } if length != uint32(len(b)) { b = make([]byte, length) } switch requestHeader.Type { case protocol.TRANSMISSION_TYPE_REQUEST_READ: if err := binary.Write(conn, binary.BigEndian, protocol.TransmissionReplyHeader{ ReplyMagic: protocol.TRANSMISSION_MAGIC_REPLY, Error: 0, Handle: requestHeader.Handle, }); err != nil { return err } n, err := export.Backend.ReadAt(b[:length], int64(requestHeader.Offset)) if err != nil { return err } if _, err := conn.Write(b[:n]); err != nil { return err } case protocol.TRANSMISSION_TYPE_REQUEST_WRITE: if options.ReadOnly { _, err := io.CopyN(io.Discard, conn, int64(requestHeader.Length)) // Discard the write command's data if err != nil { return err } if err := binary.Write(conn, binary.BigEndian, protocol.TransmissionReplyHeader{ ReplyMagic: protocol.TRANSMISSION_MAGIC_REPLY, Error: protocol.TRANSMISSION_ERROR_EPERM, Handle: requestHeader.Handle, }); err != nil { return err } break } n, err := io.ReadAtLeast(conn, b[:length], int(requestHeader.Length)) if err != nil { return err } if _, err := export.Backend.WriteAt(b[:n], int64(requestHeader.Offset)); err != nil { return err } if err := binary.Write(conn, binary.BigEndian, protocol.TransmissionReplyHeader{ ReplyMagic: protocol.TRANSMISSION_MAGIC_REPLY, Error: 0, Handle: requestHeader.Handle, }); err != nil { return err } case protocol.TRANSMISSION_TYPE_REQUEST_DISC: if !options.ReadOnly { if err := export.Backend.Sync(); err != nil { return err } } return nil default: _, err := io.CopyN(io.Discard, conn, int64(requestHeader.Length)) // Discard the unknown command's data if err != nil { return err } if err := binary.Write(conn, binary.BigEndian, protocol.TransmissionReplyHeader{ ReplyMagic: protocol.TRANSMISSION_MAGIC_REPLY, Error: protocol.TRANSMISSION_ERROR_EINVAL, Handle: requestHeader.Handle, }); err != nil { return err } } } }