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
================================================
<img alt="Project icon" style="vertical-align: middle;" src="./docs/icon.svg" width="128" height="128" align="left">
# go-nbd
Pure Go NBD server and client library.
<br/>
[](https://github.com/pojntfx/go-nbd/actions/workflows/hydrun.yaml)

[](https://pkg.go.dev/github.com/pojntfx/go-nbd)
[](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 <sys/ioctl.h>
#include <linux/nbd.h>
*/
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 <sys/ioctl.h>
#include <linux/nbd.h>
*/
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
}
}
}
}
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
SYMBOL INDEX (78 symbols across 14 files)
FILE: cmd/go-nbd-example-client/main.go
function main (line 14) | func main() {
FILE: cmd/go-nbd-example-server-file/main.go
function main (line 14) | func main() {
FILE: cmd/go-nbd-example-server-memory/main.go
function main (line 13) | func main() {
FILE: pkg/backend/backend.go
type Backend (line 5) | type Backend interface
FILE: pkg/backend/file.go
type FileBackend (line 9) | type FileBackend struct
method ReadAt (line 18) | func (b *FileBackend) ReadAt(p []byte, off int64) (n int, err error) {
method WriteAt (line 28) | func (b *FileBackend) WriteAt(p []byte, off int64) (n int, err error) {
method Size (line 38) | func (b *FileBackend) Size() (int64, error) {
method Sync (line 47) | func (b *FileBackend) Sync() error {
function NewFileBackend (line 14) | func NewFileBackend(file *os.File) *FileBackend {
FILE: pkg/backend/memory.go
type MemoryBackend (line 8) | type MemoryBackend struct
method ReadAt (line 17) | func (b *MemoryBackend) ReadAt(p []byte, off int64) (n int, err error) {
method WriteAt (line 31) | func (b *MemoryBackend) WriteAt(p []byte, off int64) (n int, err error) {
method Size (line 49) | func (b *MemoryBackend) Size() (int64, error) {
method Sync (line 53) | func (b *MemoryBackend) Sync() error {
function NewMemoryBackend (line 13) | func NewMemoryBackend(memory []byte) *MemoryBackend {
FILE: pkg/client/nbd.go
constant MinimumBlockSize (line 23) | MinimumBlockSize = 512
constant MaximumBlockSize (line 24) | MaximumBlockSize = 4096
type Options (line 38) | type Options struct
function negotiateNewstyle (line 47) | func negotiateNewstyle(conn net.Conn) error {
function Connect (line 68) | func Connect(conn net.Conn, device *os.File, options *Options) error {
function Disconnect (line 337) | func Disconnect(device *os.File) error {
function List (line 368) | func List(conn net.Conn) ([]string, error) {
FILE: pkg/ioctl/negotiation_cgo.go
constant NEGOTIATION_IOCTL_SET_SOCK (line 12) | NEGOTIATION_IOCTL_SET_SOCK = C.NBD_SET_SOCK
constant NEGOTIATION_IOCTL_SET_BLOCKSIZE (line 13) | NEGOTIATION_IOCTL_SET_BLOCKSIZE = C.NBD_SET_BLKSIZE
constant NEGOTIATION_IOCTL_SET_SIZE_BLOCKS (line 14) | NEGOTIATION_IOCTL_SET_SIZE_BLOCKS = C.NBD_SET_SIZE_BLOCKS
constant NEGOTIATION_IOCTL_DO_IT (line 15) | NEGOTIATION_IOCTL_DO_IT = C.NBD_DO_IT
constant NEGOTIATION_IOCTL_SET_TIMEOUT (line 16) | NEGOTIATION_IOCTL_SET_TIMEOUT = C.NBD_SET_TIMEOUT
FILE: pkg/ioctl/negotiation_go_amd64.go
constant NEGOTIATION_IOCTL_SET_SOCK (line 8) | NEGOTIATION_IOCTL_SET_SOCK = 43776
constant NEGOTIATION_IOCTL_SET_BLOCKSIZE (line 9) | NEGOTIATION_IOCTL_SET_BLOCKSIZE = 43777
constant NEGOTIATION_IOCTL_SET_SIZE_BLOCKS (line 10) | NEGOTIATION_IOCTL_SET_SIZE_BLOCKS = 43783
constant NEGOTIATION_IOCTL_DO_IT (line 11) | NEGOTIATION_IOCTL_DO_IT = 43779
constant NEGOTIATION_IOCTL_SET_TIMEOUT (line 12) | NEGOTIATION_IOCTL_SET_TIMEOUT = 43785
FILE: pkg/ioctl/transmission_cgo.go
constant TRANSMISSION_IOCTL_DISCONNECT (line 12) | TRANSMISSION_IOCTL_DISCONNECT = C.NBD_DISCONNECT
constant TRANSMISSION_IOCTL_CLEAR_SOCK (line 13) | TRANSMISSION_IOCTL_CLEAR_SOCK = C.NBD_CLEAR_SOCK
constant TRANSMISSION_IOCTL_CLEAR_QUE (line 14) | TRANSMISSION_IOCTL_CLEAR_QUE = C.NBD_CLEAR_QUE
FILE: pkg/ioctl/transmission_go_amd64.go
constant TRANSMISSION_IOCTL_DISCONNECT (line 6) | TRANSMISSION_IOCTL_DISCONNECT = 43784
constant TRANSMISSION_IOCTL_CLEAR_SOCK (line 7) | TRANSMISSION_IOCTL_CLEAR_SOCK = 43780
constant TRANSMISSION_IOCTL_CLEAR_QUE (line 8) | TRANSMISSION_IOCTL_CLEAR_QUE = 43781
FILE: pkg/protocol/negotiation.go
constant NEGOTIATION_MAGIC_OLDSTYLE (line 6) | NEGOTIATION_MAGIC_OLDSTYLE = uint64(0x4e42444d41474943)
constant NEGOTIATION_MAGIC_OPTION (line 7) | NEGOTIATION_MAGIC_OPTION = uint64(0x49484156454F5054)
constant NEGOTIATION_MAGIC_REPLY (line 8) | NEGOTIATION_MAGIC_REPLY = uint64(0x3e889045565a9)
constant NEGOTIATION_HANDSHAKE_FLAG_FIXED_NEWSTYLE (line 10) | NEGOTIATION_HANDSHAKE_FLAG_FIXED_NEWSTYLE = uint16(1 << 0)
constant NEGOTIATION_ID_OPTION_ABORT (line 12) | NEGOTIATION_ID_OPTION_ABORT = uint32(2)
constant NEGOTIATION_ID_OPTION_LIST (line 13) | NEGOTIATION_ID_OPTION_LIST = uint32(3)
constant NEGOTIATION_ID_OPTION_INFO (line 14) | NEGOTIATION_ID_OPTION_INFO = uint32(6)
constant NEGOTIATION_ID_OPTION_GO (line 15) | NEGOTIATION_ID_OPTION_GO = uint32(7)
constant NEGOTIATION_TYPE_REPLY_ACK (line 17) | NEGOTIATION_TYPE_REPLY_ACK = uint32(1)
constant NEGOTIATION_TYPE_REPLY_SERVER (line 18) | NEGOTIATION_TYPE_REPLY_SERVER = uint32(2)
constant NEGOTIATION_TYPE_REPLY_INFO (line 19) | NEGOTIATION_TYPE_REPLY_INFO = uint32(3)
constant NEGOTIATION_TYPE_REPLY_ERR_UNSUPPORTED (line 20) | NEGOTIATION_TYPE_REPLY_ERR_UNSUPPORTED = uint32(1 | uint32(1<<31))
constant NEGOTIATION_TYPE_REPLY_ERR_UNKNOWN (line 21) | NEGOTIATION_TYPE_REPLY_ERR_UNKNOWN = uint32(6 | uint32(1<<31))
constant NEGOTIATION_TYPE_INFO_EXPORT (line 23) | NEGOTIATION_TYPE_INFO_EXPORT = uint16(0)
constant NEGOTIATION_TYPE_INFO_NAME (line 24) | NEGOTIATION_TYPE_INFO_NAME = uint16(1)
constant NEGOTIATION_TYPE_INFO_DESCRIPTION (line 25) | NEGOTIATION_TYPE_INFO_DESCRIPTION = uint16(2)
constant NEGOTIATION_TYPE_INFO_BLOCKSIZE (line 26) | NEGOTIATION_TYPE_INFO_BLOCKSIZE = uint16(3)
constant NEGOTIATION_REPLY_FLAGS_HAS_FLAGS (line 28) | NEGOTIATION_REPLY_FLAGS_HAS_FLAGS = uint16((1 << 0))
constant NEGOTIATION_REPLY_FLAGS_CAN_MULTI_CONN (line 29) | NEGOTIATION_REPLY_FLAGS_CAN_MULTI_CONN = uint16((1 << 8))
type NegotiationNewstyleHeader (line 32) | type NegotiationNewstyleHeader struct
type NegotiationOptionHeader (line 38) | type NegotiationOptionHeader struct
type NegotiationReplyHeader (line 44) | type NegotiationReplyHeader struct
type NegotiationReplyInfo (line 51) | type NegotiationReplyInfo struct
type NegotiationReplyNameHeader (line 57) | type NegotiationReplyNameHeader struct
type NegotiationReplyDescriptionHeader (line 61) | type NegotiationReplyDescriptionHeader
type NegotiationReplyBlockSize (line 63) | type NegotiationReplyBlockSize struct
FILE: pkg/protocol/transmission.go
constant TRANSMISSION_MAGIC_REQUEST (line 4) | TRANSMISSION_MAGIC_REQUEST = uint32(0x25609513)
constant TRANSMISSION_MAGIC_REPLY (line 5) | TRANSMISSION_MAGIC_REPLY = uint32(0x67446698)
constant TRANSMISSION_TYPE_REQUEST_READ (line 7) | TRANSMISSION_TYPE_REQUEST_READ = uint16(0)
constant TRANSMISSION_TYPE_REQUEST_WRITE (line 8) | TRANSMISSION_TYPE_REQUEST_WRITE = uint16(1)
constant TRANSMISSION_TYPE_REQUEST_DISC (line 9) | TRANSMISSION_TYPE_REQUEST_DISC = uint16(2)
constant TRANSMISSION_ERROR_EPERM (line 11) | TRANSMISSION_ERROR_EPERM = uint32(1)
constant TRANSMISSION_ERROR_EINVAL (line 12) | TRANSMISSION_ERROR_EINVAL = uint32(22)
type TransmissionRequestHeader (line 15) | type TransmissionRequestHeader struct
type TransmissionReplyHeader (line 24) | type TransmissionReplyHeader struct
FILE: pkg/server/nbd.go
constant defaultMaximumRequestSize (line 20) | defaultMaximumRequestSize = 32 * 1024 * 1024
type Export (line 23) | type Export struct
type Options (line 30) | type Options struct
function Handle (line 41) | func Handle(conn net.Conn, exports []*Export, options *Options) error {
Condensed preview — 22 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (59K chars).
[
{
"path": ".github/workflows/hydrun.yaml",
"chars": 2702,
"preview": "name: hydrun CI\n\non:\n push:\n pull_request:\n schedule:\n - cron: \"0 0 * * 0\"\n\njobs:\n build-linux:\n runs-on: ${{ "
},
{
"path": ".gitignore",
"chars": 4,
"preview": "out\n"
},
{
"path": "Hydrunfile",
"chars": 201,
"preview": "#!/bin/bash\n\nset -e\n\n# Test\nif [ \"$1\" = \"test\" ]; then\n # Configure Git\n git config --global --add safe.directory '*'\n"
},
{
"path": "LICENSE",
"chars": 10280,
"preview": "Apache License\nVersion 2.0, January 2004\nhttp://www.apache.org/licenses/\n\nTERMS AND CONDITIONS FOR USE, REPRODUCTION, AN"
},
{
"path": "Makefile",
"chars": 997,
"preview": "# Public variables\nDESTDIR ?=\nPREFIX ?= /usr/local\nOUTPUT_DIR ?= out\nDST ?=\n\n# Private variables\nobj = go-nbd-example-cl"
},
{
"path": "README.md",
"chars": 6059,
"preview": "<img alt=\"Project icon\" style=\"vertical-align: middle;\" src=\"./docs/icon.svg\" width=\"128\" height=\"128\" align=\"left\">\n\n# "
},
{
"path": "cmd/go-nbd-example-client/main.go",
"chars": 1350,
"preview": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"flag\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\t\"os/signal\"\n\n\t\"github.com/pojntfx/go-nbd/pkg/client"
},
{
"path": "cmd/go-nbd-example-server-file/main.go",
"chars": 2325,
"preview": "package main\n\nimport (\n\t\"flag\"\n\t\"log\"\n\t\"net\"\n\t\"os\"\n\n\t\"github.com/pojntfx/go-nbd/pkg/backend\"\n\t\"github.com/pojntfx/go-nbd"
},
{
"path": "cmd/go-nbd-example-server-memory/main.go",
"chars": 2120,
"preview": "package main\n\nimport (\n\t\"flag\"\n\t\"log\"\n\t\"net\"\n\n\t\"github.com/pojntfx/go-nbd/pkg/backend\"\n\t\"github.com/pojntfx/go-nbd/pkg/c"
},
{
"path": "go.mod",
"chars": 87,
"preview": "module github.com/pojntfx/go-nbd\n\ngo 1.20\n\nrequire github.com/pilebones/go-udev v0.9.0\n"
},
{
"path": "go.sum",
"chars": 419,
"preview": "github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pF"
},
{
"path": "pkg/backend/backend.go",
"chars": 121,
"preview": "package backend\n\nimport \"io\"\n\ntype Backend interface {\n\tio.ReaderAt\n\tio.WriterAt\n\n\tSize() (int64, error)\n\tSync() error\n}"
},
{
"path": "pkg/backend/file.go",
"chars": 717,
"preview": "package backend\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"sync\"\n)\n\ntype FileBackend struct {\n\tfile *os.File\n\tlock sync.RWMutex\n}\n\nfunc New"
},
{
"path": "pkg/backend/memory.go",
"chars": 843,
"preview": "package backend\n\nimport (\n\t\"io\"\n\t\"sync\"\n)\n\ntype MemoryBackend struct {\n\tmemory []byte\n\tlock sync.Mutex\n}\n\nfunc NewMemo"
},
{
"path": "pkg/client/nbd.go",
"chars": 9619,
"preview": "package client\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\""
},
{
"path": "pkg/ioctl/negotiation_cgo.go",
"chars": 384,
"preview": "//go:build linux && cgo\n\npackage ioctl\n\n/*\n#include <sys/ioctl.h>\n#include <linux/nbd.h>\n*/\nimport \"C\"\n\nconst (\n\tNEGOTIA"
},
{
"path": "pkg/ioctl/negotiation_go_amd64.go",
"chars": 308,
"preview": "//go:build linux && !cgo && amd64\n\npackage ioctl\n\n// See /usr/include/linux/nbd.h\n\nconst (\n\tNEGOTIATION_IOCTL_SET_SOCK "
},
{
"path": "pkg/ioctl/transmission_cgo.go",
"chars": 263,
"preview": "//go:build linux && cgo\n\npackage ioctl\n\n/*\n#include <sys/ioctl.h>\n#include <linux/nbd.h>\n*/\nimport \"C\"\n\nconst (\n\tTRANSMI"
},
{
"path": "pkg/ioctl/transmission_go_amd64.go",
"chars": 177,
"preview": "//go:build linux && !cgo && amd64\n\npackage ioctl\n\nconst (\n\tTRANSMISSION_IOCTL_DISCONNECT = 43784\n\tTRANSMISSION_IOCTL_CLE"
},
{
"path": "pkg/protocol/negotiation.go",
"chars": 1861,
"preview": "package protocol\n\n// See https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md and https://github.com/ablig"
},
{
"path": "pkg/protocol/transmission.go",
"chars": 609,
"preview": "package protocol\n\nconst (\n\tTRANSMISSION_MAGIC_REQUEST = uint32(0x25609513)\n\tTRANSMISSION_MAGIC_REPLY = uint32(0x674466"
},
{
"path": "pkg/server/nbd.go",
"chars": 11231,
"preview": "package server\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\n\t\"github.com/pojntfx/go-nbd/pkg/backend\"\n\t\""
}
]
About this extraction
This page contains the full source code of the pojntfx/go-nbd GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 22 files (51.4 KB), approximately 14.5k tokens, and a symbol index with 78 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.