├── .gitignore ├── README.md ├── LICENSE ├── buse ├── types.go └── buse.go └── driver_example.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Output of the go coverage tool, specifically when used with LiteIDE 27 | *.out 28 | 29 | # external packages folder 30 | vendor/ 31 | 32 | buse-go 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linux block device in user space in Golang 2 | 3 | ## How to use it 4 | 5 | Checkout the file driver_example.go for a simple in-memory block device. 6 | 7 | Here is how to test, open a terminal: 8 | 9 | ``` 10 | go build 11 | sudo modprobe nbd 12 | sudo ./buse-go /dev/nbd0 13 | ``` 14 | 15 | And in another terminal: 16 | 17 | ``` 18 | mkfs.ext4 /dev/nbd0 19 | mkdir /mnt/test 20 | mount /dev/nbd0 /mnt/test 21 | echo it works > /mnt/test/foo 22 | ``` 23 | 24 | You can check out the logs in the first terminal... 25 | 26 | ## How does it work? 27 | 28 | It uses NBD (Network Block Device) behind the scene. A NBD server and client is automatically setup on the same machine. This project has been inspired by [BUSE in C](https://github.com/acozzette/BUSE). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sam Alba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /buse/types.go: -------------------------------------------------------------------------------- 1 | package buse 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // Rewrote type definitions for #defines and structs to workaround cgo 8 | // as defined in 9 | 10 | const ( 11 | NBD_SET_SOCK = (0xab<<8 | 0) 12 | NBD_SET_BLKSIZE = (0xab<<8 | 1) 13 | NBD_SET_SIZE = (0xab<<8 | 2) 14 | NBD_DO_IT = (0xab<<8 | 3) 15 | NBD_CLEAR_SOCK = (0xab<<8 | 4) 16 | NBD_CLEAR_QUE = (0xab<<8 | 5) 17 | NBD_PRINT_DEBUG = (0xab<<8 | 6) 18 | NBD_SET_SIZE_BLOCKS = (0xab<<8 | 7) 19 | NBD_DISCONNECT = (0xab<<8 | 8) 20 | NBD_SET_TIMEOUT = (0xab<<8 | 9) 21 | NBD_SET_FLAGS = (0xab<<8 | 10) 22 | ) 23 | 24 | const ( 25 | NBD_CMD_READ = 0 26 | NBD_CMD_WRITE = 1 27 | NBD_CMD_DISC = 2 28 | NBD_CMD_FLUSH = 3 29 | NBD_CMD_TRIM = 4 30 | ) 31 | 32 | const ( 33 | NBD_FLAG_HAS_FLAGS = (1 << 0) 34 | NBD_FLAG_READ_ONLY = (1 << 1) 35 | NBD_FLAG_SEND_FLUSH = (1 << 2) 36 | NBD_FLAG_SEND_TRIM = (1 << 5) 37 | ) 38 | 39 | const ( 40 | NBD_REQUEST_MAGIC = 0x25609513 41 | NBD_REPLY_MAGIC = 0x67446698 42 | ) 43 | 44 | type nbdRequest struct { 45 | Magic uint32 46 | Type uint32 47 | Handle uint64 48 | From uint64 49 | Length uint32 50 | } 51 | 52 | type nbdReply struct { 53 | Magic uint32 54 | Error uint32 55 | Handle uint64 56 | } 57 | 58 | type BuseInterface interface { 59 | ReadAt(p []byte, off uint) error 60 | WriteAt(p []byte, off uint) error 61 | Disconnect() 62 | Flush() error 63 | Trim(off uint, length uint) error 64 | } 65 | 66 | type BuseDevice struct { 67 | size uint 68 | device string 69 | driver BuseInterface 70 | deviceFp *os.File 71 | socketPair [2]int 72 | op [5]func(driver BuseInterface, fp *os.File, chunk []byte, request *nbdRequest, reply *nbdReply) error 73 | disconnect chan int 74 | } 75 | -------------------------------------------------------------------------------- /driver_example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | 10 | "github.com/samalba/buse-go/buse" 11 | ) 12 | 13 | // This device is an example implementation of an in-memory block device 14 | 15 | type DeviceExample struct { 16 | dataset []byte 17 | } 18 | 19 | func (d *DeviceExample) ReadAt(p []byte, off uint) error { 20 | copy(p, d.dataset[off:int(off)+len(p)]) 21 | log.Printf("[DeviceExample] READ offset:%d len:%d\n", off, len(p)) 22 | return nil 23 | } 24 | 25 | func (d *DeviceExample) WriteAt(p []byte, off uint) error { 26 | copy(d.dataset[off:], p) 27 | log.Printf("[DeviceExample] WRITE offset:%d len:%d\n", off, len(p)) 28 | return nil 29 | } 30 | 31 | func (d *DeviceExample) Disconnect() { 32 | log.Println("[DeviceExample] DISCONNECT") 33 | } 34 | 35 | func (d *DeviceExample) Flush() error { 36 | log.Println("[DeviceExample] FLUSH") 37 | return nil 38 | } 39 | 40 | func (d *DeviceExample) Trim(off, length uint) error { 41 | log.Printf("[DeviceExample] TRIM offset:%d len:%d\n", off, length) 42 | return nil 43 | } 44 | 45 | func usage() { 46 | fmt.Fprintf(os.Stderr, "usage: %s /dev/nbd0\n", os.Args[0]) 47 | flag.PrintDefaults() 48 | os.Exit(2) 49 | } 50 | 51 | func main() { 52 | flag.Usage = usage 53 | flag.Parse() 54 | args := flag.Args() 55 | if len(args) < 1 { 56 | usage() 57 | } 58 | size := uint(1024 * 1024 * 512) // 512M 59 | deviceExp := &DeviceExample{} 60 | deviceExp.dataset = make([]byte, size) 61 | device, err := buse.CreateDevice(args[0], size, deviceExp) 62 | if err != nil { 63 | fmt.Printf("Cannot create device: %s\n", err) 64 | os.Exit(1) 65 | } 66 | sig := make(chan os.Signal) 67 | signal.Notify(sig, os.Interrupt) 68 | go func() { 69 | if err := device.Connect(); err != nil { 70 | log.Printf("Buse device stopped with error: %s", err) 71 | } else { 72 | log.Println("Buse device stopped gracefully.") 73 | } 74 | }() 75 | <-sig 76 | // Received SIGTERM, cleanup 77 | fmt.Println("SIGINT, disconnecting...") 78 | device.Disconnect() 79 | } 80 | -------------------------------------------------------------------------------- /buse/buse.go: -------------------------------------------------------------------------------- 1 | package buse 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "syscall" 10 | "unsafe" 11 | ) 12 | 13 | func ioctl(fd, op, arg uintptr) { 14 | _, _, ep := syscall.Syscall(syscall.SYS_IOCTL, fd, op, arg) 15 | if ep != 0 { 16 | log.Fatalf("ioctl(%d, %d, %d) failed: %s", fd, op, arg, syscall.Errno(ep)) 17 | } 18 | } 19 | 20 | func opDeviceRead(driver BuseInterface, fp *os.File, chunk []byte, request *nbdRequest, reply *nbdReply) error { 21 | if err := driver.ReadAt(chunk, uint(request.From)); err != nil { 22 | log.Println("buseDriver.ReadAt returned an error:", err) 23 | // Reply with an EPERM 24 | reply.Error = 1 25 | } 26 | buf := writeNbdReply(reply) 27 | if _, err := fp.Write(buf); err != nil { 28 | log.Println("Write error, when sending reply header:", err) 29 | } 30 | if _, err := fp.Write(chunk); err != nil { 31 | log.Println("Write error, when sending data chunk:", err) 32 | } 33 | return nil 34 | } 35 | 36 | func opDeviceWrite(driver BuseInterface, fp *os.File, chunk []byte, request *nbdRequest, reply *nbdReply) error { 37 | if _, err := io.ReadFull(fp, chunk); err != nil { 38 | return fmt.Errorf("Fatal error, cannot read request packet: %s", err) 39 | } 40 | if err := driver.WriteAt(chunk, uint(request.From)); err != nil { 41 | log.Println("buseDriver.WriteAt returned an error:", err) 42 | reply.Error = 1 43 | } 44 | buf := writeNbdReply(reply) 45 | if _, err := fp.Write(buf); err != nil { 46 | log.Println("Write error, when sending reply header:", err) 47 | } 48 | return nil 49 | } 50 | 51 | func opDeviceDisconnect(driver BuseInterface, fp *os.File, chunk []byte, request *nbdRequest, reply *nbdReply) error { 52 | log.Println("Calling buseDriver.Disconnect()") 53 | driver.Disconnect() 54 | return fmt.Errorf("Received a disconnect") 55 | } 56 | 57 | func opDeviceFlush(driver BuseInterface, fp *os.File, chunk []byte, request *nbdRequest, reply *nbdReply) error { 58 | if err := driver.Flush(); err != nil { 59 | log.Println("buseDriver.Flush returned an error:", err) 60 | reply.Error = 1 61 | } 62 | buf := writeNbdReply(reply) 63 | if _, err := fp.Write(buf); err != nil { 64 | log.Println("Write error, when sending reply header:", err) 65 | } 66 | return nil 67 | } 68 | 69 | func opDeviceTrim(driver BuseInterface, fp *os.File, chunk []byte, request *nbdRequest, reply *nbdReply) error { 70 | if err := driver.Trim(uint(request.From), uint(request.Length)); err != nil { 71 | log.Println("buseDriver.Flush returned an error:", err) 72 | reply.Error = 1 73 | } 74 | buf := writeNbdReply(reply) 75 | if _, err := fp.Write(buf); err != nil { 76 | log.Println("Write error, when sending reply header:", err) 77 | } 78 | return nil 79 | } 80 | 81 | func (bd *BuseDevice) startNBDClient() { 82 | ioctl(bd.deviceFp.Fd(), NBD_SET_SOCK, uintptr(bd.socketPair[1])) 83 | // The call below may fail on some systems (if flags unset), could be ignored 84 | ioctl(bd.deviceFp.Fd(), NBD_SET_FLAGS, NBD_FLAG_SEND_TRIM) 85 | // The following call will block until the client disconnects 86 | log.Println("Starting NBD client...") 87 | go ioctl(bd.deviceFp.Fd(), NBD_DO_IT, 0) 88 | // Block on the disconnect channel 89 | <-bd.disconnect 90 | } 91 | 92 | // Disconnect disconnects the BuseDevice 93 | func (bd *BuseDevice) Disconnect() { 94 | bd.disconnect <- 1 95 | // Ok to fail, ignore errors 96 | syscall.Syscall(syscall.SYS_IOCTL, bd.deviceFp.Fd(), NBD_CLEAR_QUE, 0) 97 | syscall.Syscall(syscall.SYS_IOCTL, bd.deviceFp.Fd(), NBD_DISCONNECT, 0) 98 | syscall.Syscall(syscall.SYS_IOCTL, bd.deviceFp.Fd(), NBD_CLEAR_SOCK, 0) 99 | // Cleanup fd 100 | syscall.Close(bd.socketPair[0]) 101 | syscall.Close(bd.socketPair[1]) 102 | bd.deviceFp.Close() 103 | log.Println("NBD client disconnected") 104 | } 105 | 106 | func readNbdRequest(buf []byte, request *nbdRequest) { 107 | request.Magic = binary.BigEndian.Uint32(buf) 108 | request.Type = binary.BigEndian.Uint32(buf[4:8]) 109 | request.Handle = binary.BigEndian.Uint64(buf[8:16]) 110 | request.From = binary.BigEndian.Uint64(buf[16:24]) 111 | request.Length = binary.BigEndian.Uint32(buf[24:28]) 112 | } 113 | 114 | func writeNbdReply(reply *nbdReply) []byte { 115 | buf := make([]byte, unsafe.Sizeof(*reply)) 116 | binary.BigEndian.PutUint32(buf[0:4], NBD_REPLY_MAGIC) 117 | binary.BigEndian.PutUint32(buf[4:8], reply.Error) 118 | binary.BigEndian.PutUint64(buf[8:16], reply.Handle) 119 | // NOTE: a struct in go has 4 extra bytes, so we skip the last 120 | return buf[0:16] 121 | } 122 | 123 | // Connect connects a BuseDevice to an actual device file 124 | // and starts handling requests. It does not return until it's done serving requests. 125 | func (bd *BuseDevice) Connect() error { 126 | go bd.startNBDClient() 127 | defer bd.Disconnect() 128 | //opens the device file at least once, to make sure the partition table is updated 129 | tmp, err := os.Open(bd.device) 130 | if err != nil { 131 | return fmt.Errorf("Cannot reach the device %s: %s", bd.device, err) 132 | } 133 | tmp.Close() 134 | // Start handling requests 135 | request := nbdRequest{} 136 | reply := nbdReply{Magic: NBD_REPLY_MAGIC} 137 | fp := os.NewFile(uintptr(bd.socketPair[0]), "unix") 138 | // NOTE: a struct in go has 4 extra bytes... 139 | buf := make([]byte, unsafe.Sizeof(request)) 140 | for true { 141 | if _, err := fp.Read(buf[0:28]); err != nil { 142 | return fmt.Errorf("NBD client stopped: %s", err) 143 | } 144 | readNbdRequest(buf, &request) 145 | fmt.Printf("DEBUG %#v\n", request) 146 | if request.Magic != NBD_REQUEST_MAGIC { 147 | return fmt.Errorf("Fatal error: received packet with wrong Magic number") 148 | } 149 | reply.Handle = request.Handle 150 | chunk := make([]byte, request.Length) 151 | reply.Error = 0 152 | // Dispatches READ, WRITE, DISC, FLUSH, TRIM to the corresponding implementation 153 | if request.Type < NBD_CMD_READ || request.Type > NBD_CMD_TRIM { 154 | log.Println("Received unknown request:", request.Type) 155 | continue 156 | } 157 | if err := bd.op[request.Type](bd.driver, fp, chunk, &request, &reply); err != nil { 158 | return err 159 | } 160 | } 161 | return nil 162 | } 163 | 164 | func CreateDevice(device string, size uint, buseDriver BuseInterface) (*BuseDevice, error) { 165 | buseDevice := &BuseDevice{size: size, device: device, driver: buseDriver} 166 | sockPair, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) 167 | if err != nil { 168 | return nil, fmt.Errorf("Call to socketpair failed: %s", err) 169 | } 170 | fp, err := os.OpenFile(device, os.O_RDWR, 0600) 171 | if err != nil { 172 | return nil, fmt.Errorf("Cannot open \"%s\". Make sure the `nbd' kernel module is loaded: %s", device, err) 173 | } 174 | buseDevice.deviceFp = fp 175 | ioctl(buseDevice.deviceFp.Fd(), NBD_SET_SIZE, uintptr(size)) 176 | ioctl(buseDevice.deviceFp.Fd(), NBD_CLEAR_QUE, 0) 177 | ioctl(buseDevice.deviceFp.Fd(), NBD_CLEAR_SOCK, 0) 178 | buseDevice.socketPair = sockPair 179 | buseDevice.op[NBD_CMD_READ] = opDeviceRead 180 | buseDevice.op[NBD_CMD_WRITE] = opDeviceWrite 181 | buseDevice.op[NBD_CMD_DISC] = opDeviceDisconnect 182 | buseDevice.op[NBD_CMD_FLUSH] = opDeviceFlush 183 | buseDevice.op[NBD_CMD_TRIM] = opDeviceTrim 184 | buseDevice.disconnect = make(chan int, 5) 185 | return buseDevice, nil 186 | } 187 | --------------------------------------------------------------------------------