TFTP server and client library for Golang
Implements:
Partially implements (tsize server side only):
- RFC 2349 - TFTP Timeout Interval and Transfer Size Options
Set of features is sufficient for PXE boot support.
import "github.com/pin/tftp"
The package is cohesive to Golang io
. Particularly it implements
io.ReaderFrom
and io.WriterTo
interfaces. That allows efficient data
transmission without unnecessary memory copying and allocations.
TFTP Server
func readHandler(filename string, rf io.ReaderFrom) error {
file, err := os.Open(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
n, err := rf.ReadFrom(file)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
fmt.Printf("%d bytes sent\n", n)
return nil
}
func writeHandler(filename string, wt io.WriterTo) error {
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
n, err := wt.WriteTo(file)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
return err
}
fmt.Printf("%d bytes received\n", n)
return nil
}
func main() {
s := tftp.NewServer(readHandler, writeHandler)
s.SetTimeout(5 * time.Second)
err := s.ListenAndServe(":69")
if err != nil {
fmt.Fprintf(os.Stdout, "server: %v\n", err)
os.Exit(1)
}
}
TFTP Client
Upload file to server:
c, err := tftp.NewClient("172.16.4.21:69")
file, err := os.Open(path)
c.SetTimeout(5 * time.Second)
rf, err := c.Send("foobar.txt", "octet")
n, err := rf.ReadFrom(file)
fmt.Printf("%d bytes sent\n", n)
Download file from server:
c, err := tftp.NewClient("172.16.4.21:69")
wt, err := c.Receive("foobar.txt", "octet")
file, err := os.Create(path)
if n, ok := wt.(IncomingTransfer).Size(); ok {
fmt.Printf("Transfer size: %d\n", n)
}
n, err := wt.WriteTo(file)
fmt.Printf("%d bytes received\n", n)
Note: please handle errors better :)
TSize option
PXE boot ROM often expects tsize option support from a server: client
(e.g. computer that boots over the network) wants to know size of a
download before the actual data comes. Server has to obtain stream
size and send it to a client.
Often it will happen automatically because TFTP library tries to check
if io.Reader
provided to ReadFrom
method also satisfies
io.Seeker
interface (os.File
for instance) and uses Seek
to
determine file size.
In case io.Reader
you provide to ReadFrom
in read handler does not
satisfy io.Seeker
interface or you do not want TFTP library to call
Seek
on your reader but still want to respond with tsize option
during outgoing request you can use an OutgoingTransfer
interface:
func readHandler(filename string, rf io.ReaderFrom) error {
...
rf.(tftp.OutgoingTransfer).SetSize(myFileSize)
...
Similarly, it is possible to obtain size of a file that is about to be
received using IncomingTransfer
interface (see Size
method).
Local and Remote Address
The OutgoingTransfer
and IncomingTransfer
interfaces also provide
the RemoteAddr
method which returns the peer IP address and port as
a net.UDPAddr
. The RequestPacketInfo
interface provides a
LocalIP
method with returns the local IP address as a net.IP
that
the request is being handled on. These can be used for detailed
logging in a server handler, among other things.
Note that LocalIP may return nil or an unspecified IP address
if finding that is not supported on a particular operating system by
the Go net libraries, or if you call it as a TFTP client.
func readHandler(filename string, rf io.ReaderFrom) error {
...
raddr := rf.(tftp.OutgoingTransfer).RemoteAddr()
laddr := rf.(tftp.RequestPacketInfo).LocalIP()
log.Println("RRQ from", raddr.String(), "To ",laddr.String())
log.Println("")
...
Backoff
The default backoff before retransmitting an unacknowledged packet is a
random duration between 0 and 1 second. This behavior can be overridden
in clients and servers by providing a custom backoff calculation function.
s := tftp.NewServer(readHandler, writeHandler)
s.SetBackoff(func (attempts int) time.Duration {
return time.Duration(attempts) * time.Second
})
or, for no backoff
s.SetBackoff(func (int) time.Duration { return 0 })