diff --git a/http/tracker.go b/http/tracker.go index b3e4851..6e7e95d 100644 --- a/http/tracker.go +++ b/http/tracker.go @@ -38,17 +38,34 @@ func (s *Server) newAnnounce(r *http.Request, p httprouter.Params) (*models.Anno return nil, models.ErrMalformedRequest } - ipv4, ipv6, err := requestedIP(q, r, &s.config.NetConfig) - if err != nil { - return nil, models.ErrMalformedRequest - } - port, err := q.Uint64("port") if err != nil { return nil, models.ErrMalformedRequest } left, err := q.Uint64("left") + + ipv4, ipv6, err := requestedEndpoint(q, r, &s.config.NetConfig) + if err != nil { + return nil, models.ErrMalformedRequest + } + + var ipv4Endpoint, ipv6Endpoint models.Endpoint + if ipv4 != nil { + ipv4Endpoint = *ipv4 + // If the port we couldn't get the port before, fallback to the port param. + if ipv4Endpoint.Port == uint16(0) { + ipv4Endpoint.Port = uint16(port) + } + } + if ipv6 != nil { + ipv6Endpoint = *ipv6 + // If the port we couldn't get the port before, fallback to the port param. + if ipv6Endpoint.Port == uint16(0) { + ipv6Endpoint.Port = uint16(port) + } + } + if err != nil { return nil, models.ErrMalformedRequest } @@ -68,14 +85,13 @@ func (s *Server) newAnnounce(r *http.Request, p httprouter.Params) (*models.Anno Compact: compact, Downloaded: downloaded, Event: event, - IPv4: ipv4, - IPv6: ipv6, + IPv4: ipv4Endpoint, + IPv6: ipv6Endpoint, Infohash: infohash, Left: left, NumWant: numWant, Passkey: p.ByName("passkey"), PeerID: peerID, - Port: uint16(port), Uploaded: uploaded, }, nil } @@ -116,26 +132,26 @@ func requestedPeerCount(q *query.Query, fallback int) int { return fallback } -// requestedIP returns the IP addresses for a request. If there are multiple -// IP addresses in the request, one IPv4 and one IPv6 will be returned. -func requestedIP(q *query.Query, r *http.Request, cfg *config.NetConfig) (v4, v6 net.IP, err error) { +// requestedEndpoint returns the IP address and port pairs for a request. If +// there are multiple in the request, one IPv4 and one IPv6 will be returned. +func requestedEndpoint(q *query.Query, r *http.Request, cfg *config.NetConfig) (v4, v6 *models.Endpoint, err error) { var done bool if cfg.AllowIPSpoofing { if str, ok := q.Params["ip"]; ok { - if v4, v6, done = getIPs(str, v4, v6, cfg); done { + if v4, v6, done = getEndpoints(str, v4, v6, cfg); done { return } } if str, ok := q.Params["ipv4"]; ok { - if v4, v6, done = getIPs(str, v4, v6, cfg); done { + if v4, v6, done = getEndpoints(str, v4, v6, cfg); done { return } } if str, ok := q.Params["ipv6"]; ok { - if v4, v6, done = getIPs(str, v4, v6, cfg); done { + if v4, v6, done = getEndpoints(str, v4, v6, cfg); done { return } } @@ -143,47 +159,49 @@ func requestedIP(q *query.Query, r *http.Request, cfg *config.NetConfig) (v4, v6 if cfg.RealIPHeader != "" { if xRealIPs, ok := r.Header[cfg.RealIPHeader]; ok { - if v4, v6, done = getIPs(string(xRealIPs[0]), v4, v6, cfg); done { + if v4, v6, done = getEndpoints(string(xRealIPs[0]), v4, v6, cfg); done { return } } } else { - if r.RemoteAddr == "" { - if v4 == nil { - v4 = net.ParseIP("127.0.0.1") - } - return - } - - var host string - host, _, err = net.SplitHostPort(r.RemoteAddr) - - if err == nil && host != "" { - if v4, v6, done = getIPs(host, v4, v6, cfg); done { + if r.RemoteAddr == "" && v4 == nil { + if v4, v6, done = getEndpoints("127.0.0.1", v4, v6, cfg); done { return } } + + if v4, v6, done = getEndpoints(r.RemoteAddr, v4, v6, cfg); done { + return + } } if v4 == nil && v6 == nil { err = errors.New("failed to parse IP address") } + return } -func getIPs(ipstr string, ipv4, ipv6 net.IP, cfg *config.NetConfig) (net.IP, net.IP, bool) { - var done bool +func getEndpoints(ipstr string, ipv4, ipv6 *models.Endpoint, cfg *config.NetConfig) (*models.Endpoint, *models.Endpoint, bool) { + host, port, err := net.SplitHostPort(ipstr) + if err != nil { + host = ipstr + } - if ip := net.ParseIP(ipstr); ip != nil { - newIPv4 := ip.To4() + // We can ignore this error, because ports that are 0 are assumed to be the + // port parameter provided in the "port" param of the announce request. + parsedPort, _ := strconv.ParseUint(port, 10, 16) - if ipv4 == nil && newIPv4 != nil { - ipv4 = newIPv4 - } else if ipv6 == nil && newIPv4 == nil { - ipv6 = ip + if ip := net.ParseIP(host); ip != nil { + ipTo4 := ip.To4() + if ipv4 == nil && ipTo4 != nil { + ipv4 = &models.Endpoint{ipTo4, uint16(parsedPort)} + } else if ipv6 == nil && ipTo4 == nil { + ipv6 = &models.Endpoint{ip, uint16(parsedPort)} } } + var done bool if cfg.DualStackedPeers { done = ipv4 != nil && ipv6 != nil } else { diff --git a/tracker/models/models.go b/tracker/models/models.go index d10014e..57a20b1 100644 --- a/tracker/models/models.go +++ b/tracker/models/models.go @@ -8,7 +8,6 @@ package models import ( "net" - "strconv" "strings" "time" @@ -46,6 +45,7 @@ func (e ClientError) Error() string { return string(e) } func (e NotFoundError) Error() string { return string(e) } func (e ProtocolError) Error() string { return string(e) } +// IsPublicError determines whether an error should be propogated to the client. func IsPublicError(err error) bool { _, cl := err.(ClientError) _, nf := err.(NotFoundError) @@ -60,8 +60,8 @@ type PeerList []Peer type PeerKey string // NewPeerKey creates a properly formatted PeerKey. -func NewPeerKey(peerID string, ip net.IP, port string) PeerKey { - return PeerKey(peerID + "//" + ip.String() + ":" + port) +func NewPeerKey(peerID string, ip net.IP) PeerKey { + return PeerKey(peerID + "//" + ip.String()) } // IP parses and returns the IP address for a given PeerKey. @@ -78,26 +78,23 @@ func (pk PeerKey) PeerID() string { return strings.Split(string(pk), "//")[0] } -// Port returns the port section of the PeerKey. -func (pk PeerKey) Port() string { - return strings.Split(string(pk), "//")[2] +// Endpoint is an IP and port pair. +type Endpoint struct { + // Always has length net.IPv4len if IPv4, and net.IPv6len if IPv6 + IP net.IP `json:"ip"` + Port uint16 `json:"port"` } // Peer is a participant in a swarm. type Peer struct { - ID string `json:"id"` - UserID uint64 `json:"user_id"` - TorrentID uint64 `json:"torrent_id"` - - // Always has length net.IPv4len if IPv4, and net.IPv6len if IPv6 - IP net.IP `json:"ip,omitempty"` - - Port uint16 `json:"port"` - + ID string `json:"id"` + UserID uint64 `json:"user_id"` + TorrentID uint64 `json:"torrent_id"` Uploaded uint64 `json:"uploaded"` Downloaded uint64 `json:"downloaded"` Left uint64 `json:"left"` LastAnnounce int64 `json:"last_announce"` + Endpoint } // HasIPv4 determines if a peer's IP address can be represented as an IPv4 @@ -114,7 +111,7 @@ func (p *Peer) HasIPv6() bool { // Key returns a PeerKey for the given peer. func (p *Peer) Key() PeerKey { - return NewPeerKey(p.ID, p.IP, strconv.FormatUint(p.Port, 10)) + return NewPeerKey(p.ID, p.IP) } // Torrent is a swarm for a given torrent file. @@ -149,18 +146,17 @@ type User struct { type Announce struct { Config *config.Config `json:"config"` - Compact bool `json:"compact"` - Downloaded uint64 `json:"downloaded"` - Event string `json:"event"` - IPv4 net.IP `json:"ipv4"` - IPv6 net.IP `json:"ipv6"` - Infohash string `json:"infohash"` - Left uint64 `json:"left"` - NumWant int `json:"numwant"` - Passkey string `json:"passkey"` - PeerID string `json:"peer_id"` - Port uint16 `json:"port"` - Uploaded uint64 `json:"uploaded"` + Compact bool `json:"compact"` + Downloaded uint64 `json:"downloaded"` + Event string `json:"event"` + IPv4 Endpoint `json:"ipv4"` + IPv6 Endpoint `json:"ipv6"` + Infohash string `json:"infohash"` + Left uint64 `json:"left"` + NumWant int `json:"numwant"` + Passkey string `json:"passkey"` + PeerID string `json:"peer_id"` + Uploaded uint64 `json:"uploaded"` Torrent *Torrent `json:"-"` User *User `json:"-"` @@ -186,12 +182,14 @@ func (a *Announce) ClientID() (clientID string) { return } +// HasIPv4 determines whether or not an announce has an IPv4 endpoint. func (a *Announce) HasIPv4() bool { - return a.IPv4 != nil + return a.IPv4.IP != nil } +// HasIPv6 determines whether or not an announce has an IPv6 endpoint. func (a *Announce) HasIPv6() bool { - return a.IPv6 != nil + return a.IPv6.IP != nil } // BuildPeer creates the Peer representation of an Announce. When provided nil @@ -201,7 +199,6 @@ func (a *Announce) HasIPv6() bool { func (a *Announce) BuildPeer(u *User, t *Torrent) { a.Peer = &Peer{ ID: a.PeerID, - Port: a.Port, Uploaded: a.Uploaded, Downloaded: a.Downloaded, Left: a.Left, @@ -220,15 +217,15 @@ func (a *Announce) BuildPeer(u *User, t *Torrent) { if a.HasIPv4() && a.HasIPv6() { a.PeerV4 = a.Peer - a.PeerV4.IP = a.IPv4 + a.PeerV4.Endpoint = a.IPv4 a.PeerV6 = &*a.Peer - a.PeerV6.IP = a.IPv6 + a.PeerV6.Endpoint = a.IPv6 } else if a.HasIPv4() { a.PeerV4 = a.Peer - a.PeerV4.IP = a.IPv4 + a.PeerV4.Endpoint = a.IPv4 } else if a.HasIPv6() { a.PeerV6 = a.Peer - a.PeerV6.IP = a.IPv6 + a.PeerV6.Endpoint = a.IPv6 } else { panic("models: announce must have an IP") } diff --git a/udp/protocol.go b/udp/protocol.go index f089663..ae40632 100644 --- a/udp/protocol.go +++ b/udp/protocol.go @@ -24,7 +24,17 @@ var ( // initialConnectionID is the magic initial connection ID specified by BEP 15. initialConnectionID = []byte{0, 0, 0x04, 0x17, 0x27, 0x10, 0x19, 0x80} - // eventIDs maps IDs to event names. + // emptyIPs are the value of an IP field that has been left blank. + emptyIPv4 = []byte{0, 0, 0, 0} + emptyIPv6 = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + + // Option-Types described in BEP41 and BEP45. + optionEndOfOptions = byte(0x0) + optionNOP = byte(0x1) + optionURLData = byte(0x2) + optionIPv6 = byte(0x3) + + // eventIDs map IDs to event names. eventIDs = []string{ "", "completed", @@ -125,9 +135,9 @@ func (s *Server) newAnnounce(packet []byte, ip net.IP) (*models.Announce, error) return nil, errMalformedEvent } - ipbytes := packet[84:88] - if s.config.AllowIPSpoofing && !bytes.Equal(ipbytes, []byte{0, 0, 0, 0}) { - ip = net.ParseIP(string(ipbytes)) + ipv4bytes := packet[84:88] + if s.config.AllowIPSpoofing && !bytes.Equal(ipv4bytes, emptyIPv4) { + ip = net.ParseIP(string(ipv4bytes)) } if ip == nil { @@ -139,16 +149,65 @@ func (s *Server) newAnnounce(packet []byte, ip net.IP) (*models.Announce, error) numWant := binary.BigEndian.Uint32(packet[92:96]) port := binary.BigEndian.Uint16(packet[96:98]) + // Optionally, parse the optional parameteres as described in BEP41. + var IPv6Endpoint models.Endpoint + if len(packet) > 98 { + optionStartIndex := 98 + for optionStartIndex < len(packet)-1 { + option := packet[optionStartIndex] + switch option { + case optionEndOfOptions: + break + + case optionNOP: + optionStartIndex++ + + case optionURLData: + if optionStartIndex+1 > len(packet)-1 { + return nil, errMalformedPacket + } + + length := int(packet[optionStartIndex+1]) + if optionStartIndex+1+length > len(packet)-1 { + return nil, errMalformedPacket + } + + // TODO: Actually parse the URL Data as described in BEP41. + + optionStartIndex += 1 + length + + case optionIPv6: + if optionStartIndex+19 > len(packet)-1 { + return nil, errMalformedPacket + } + + ipv6bytes := packet[optionStartIndex+1 : optionStartIndex+17] + if s.config.AllowIPSpoofing && !bytes.Equal(ipv6bytes, emptyIPv6) { + IPv6Endpoint.IP = net.ParseIP(string(ipv6bytes)).To16() + IPv6Endpoint.Port = binary.BigEndian.Uint16(packet[optionStartIndex+17 : optionStartIndex+19]) + if IPv6Endpoint.IP == nil { + return nil, errMalformedIP + } + } + + optionStartIndex += 19 + + default: + break + } + } + } + return &models.Announce{ Config: s.config, Downloaded: downloaded, Event: eventIDs[eventID], - IPv4: ip, + IPv4: models.Endpoint{ip, port}, + IPv6: IPv6Endpoint, Infohash: string(infohash), Left: left, NumWant: int(numWant), PeerID: string(peerID), - Port: port, Uploaded: uploaded, }, nil }