diff --git a/dhcp/BUILD.bazel b/dhcp/BUILD.bazel index f608dc6..0672ead 100644 --- a/dhcp/BUILD.bazel +++ b/dhcp/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "options.go", "packet.go", + "read_options.go", ], importpath = "git.wntrmute.dev/kyle/kdhcp/dhcp", visibility = ["//visibility:public"], diff --git a/dhcp/options.go b/dhcp/options.go index 288f14e..59f0b36 100644 --- a/dhcp/options.go +++ b/dhcp/options.go @@ -1,57 +1,72 @@ package dhcp import ( - "errors" "fmt" "io" ) type OptionTag uint8 +func (opt OptionTag) String() string { + s, ok := optionStrings[opt] + if !ok { + panic(fmt.Sprintf("no string for option %d", opt)) + } + + return s +} + type Option func(req *BootRequest, r io.Reader) error const ( OptionTagPadding OptionTag = 0 + OptionTagSubnetMask OptionTag = 1 + OptionTagTimeOffset OptionTag = 2 + OptionTagRouter OptionTag = 3 + OptionTagDomainNameServer OptionTag = 6 OptionTagHostName OptionTag = 12 + OptionTagDomainName OptionTag = 15 + OptionTagInterfaceMTU OptionTag = 26 + OptionTagBroadcastAddress OptionTag = 28 + OptionTagNTPServers OptionTag = 42 + OptionTagNBNS OptionTag = 44 + OptionTagNBScope OptionTag = 47 OptionTagMessageType OptionTag = 53 OptionTagParameterRequestList OptionTag = 55 + OptionTagDomainSearch OptionTag = 119 + OptionTagClasslessStaticRoute OptionTag = 121 OptionTagEnd OptionTag = 255 ) -var optionRegistry = map[OptionTag]Option{ - OptionTagPadding: OptionPad, - OptionTagHostName: OptionHostName, - OptionTagMessageType: OptionMessageType, - OptionTagParameterRequestList: OptionParameterRequestList, - OptionTagEnd: OptionEnd, +var optionStrings = map[OptionTag]string{ + OptionTagPadding: "pad", + OptionTagSubnetMask: "subnet mask", + OptionTagTimeOffset: "time offset from UTC", + OptionTagRouter: "routers", + OptionTagDomainNameServer: "domain name servers", + OptionTagHostName: "host name", + OptionTagDomainName: "domain name", + OptionTagInterfaceMTU: "interface MTU", + OptionTagBroadcastAddress: "broadcast address", + OptionTagNTPServers: "NTP servers", + OptionTagNBNS: "NetBIOS name servers", + OptionTagNBScope: "NetBIOS scope", + OptionTagMessageType: "message type", + OptionTagParameterRequestList: "parameter request list", + OptionTagDomainSearch: "search domain", + OptionTagClasslessStaticRoute: "classless static route", + OptionTagEnd: "end", } -func OptionPad(req *BootRequest, r io.Reader) error { - return nil -} +type DHCPMessageType uint8 -func OptionHostName(req *BootRequest, r io.Reader) error { - return errors.New("dhcp: option not implemented yet") -} - -func OptionMessageType(req *BootRequest, r io.Reader) error { - return errors.New("dhcp: option not implemented yet") -} - -func OptionParameterRequestList(req *BootRequest, r io.Reader) error { - return errors.New("dhcp: option not implemented yet") -} - -func OptionEnd(req *BootRequest, r io.Reader) error { - return errors.New("dhcp: option not implemented yet") -} - -func ReadOption(req *BootRequest, tag byte, r io.Reader) error { - opt := OptionTag(tag) - if f, ok := optionRegistry[opt]; ok { - return f(req, r) - - } - - return fmt.Errorf("dhcp: unknown/unhandled option %d", opt) -} +const ( + DHCPMessageTypeDiscover DHCPMessageType = 1 + DHCPMessageTypeOffer DHCPMessageType = 2 + DHCPMessageTypeRequest DHCPMessageType = 3 + DHCPMessageTypeDecline DHCPMessageType = 4 + DHCPMessageTypeAck DHCPMessageType = 5 + DHCPMessageTypeNAK DHCPMessageType = 6 + DHCPMessageTypeRelease DHCPMessageType = 7 + DHCPMessageTypeInform DHCPMessageType = 8 +) diff --git a/dhcp/packet.go b/dhcp/packet.go index ecae000..182dfbb 100644 --- a/dhcp/packet.go +++ b/dhcp/packet.go @@ -44,16 +44,21 @@ type BootRequest struct { NextIP net.IP RelayIP net.IP - DHCPType uint8 // option 53 - HostName string // option 12 - ParameterRequests []string + DHCPType DHCPMessageType // option 53 + HostName string // option 12 + ParameterRequests []OptionTag + endOptions bool +} + +func newPacketReaderFunc(r io.Reader) func(v any) error { + return func(v any) error { + return binary.Read(r, binary.BigEndian, v) + } } func (req *BootRequest) Read(packet []byte) error { buf := bytes.NewBuffer(packet) - read := func(v any) error { - return binary.Read(buf, binary.BigEndian, v) - } + read := newPacketReaderFunc(buf) if err := read(&req.MessageType); err != nil { return err @@ -113,7 +118,6 @@ func (req *BootRequest) Read(packet []byte) error { if _, err := buf.Read(hwaPad); err != nil { return err } - log.Debugf("padding: %x", hwaPad) tempBuf := make([]byte, maxServerName) if _, err := buf.Read(tempBuf); err != nil { @@ -127,7 +131,15 @@ func (req *BootRequest) Read(packet []byte) error { } req.FileName = string(bytes.Trim(tempBuf, "\x00")) + if err := ReadMagicCookie(buf); err != nil { + return err + } + for { + if req.endOptions { + break + } + tag, err := buf.ReadByte() if err != nil { if err == io.EOF { @@ -138,6 +150,8 @@ func (req *BootRequest) Read(packet []byte) error { err = ReadOption(req, tag, buf) if err != nil { + log.Spew(*req) + log.Spew(packet) return err } } diff --git a/dhcp/read_options.go b/dhcp/read_options.go new file mode 100644 index 0000000..7f7e3cc --- /dev/null +++ b/dhcp/read_options.go @@ -0,0 +1,133 @@ +package dhcp + +import ( + "bytes" + "errors" + "fmt" + "io" + + log "git.wntrmute.dev/kyle/goutils/syslog" +) + +var optionRegistry = map[OptionTag]Option{ + OptionTagPadding: OptionPad, + OptionTagHostName: OptionHostName, + OptionTagMessageType: OptionMessageType, + OptionTagParameterRequestList: OptionParameterRequestList, + OptionTagEnd: OptionEnd, +} + +func OptionPad(req *BootRequest, r io.Reader) error { + // The padding option is a single 0 byte octet. + return nil +} + +// OptionHostName reads a DHCP host name option. +// +// 3.14. Host Name Option +// +// This option specifies the name of the client. The name may or may +// not be qualified with the local domain name (see section 3.17 for the +// preferred way to retrieve the domain name). See RFC 1035 for +// character set restrictions. + +// The code for this option is 12, and its minimum length is 1. +func OptionHostName(req *BootRequest, r io.Reader) error { + read := newPacketReaderFunc(r) + + var length uint8 + if err := read(&length); err != nil { + return fmt.Errorf("dhcp: reading option length for DHCP Message Type") + } else if length == 0 { + return errors.New("dhcp: read option length 0, but expected option length for DHCP Host Name is >= 1") + } + + hostName := make([]byte, int(length)) + if n, err := r.Read(hostName); err != nil { + return fmt.Errorf("dhcp: while reading hostname: %w", err) + } else if n != int(length) { + return fmt.Errorf("dhcp: only read %d bytes of hostname, expected %d bytes", n, length) + } + + req.HostName = string(hostName) + return nil +} + +func OptionMessageType(req *BootRequest, r io.Reader) error { + read := newPacketReaderFunc(r) + + var length uint8 + if err := read(&length); err != nil { + return fmt.Errorf("dhcp: reading option length for DHCP Message Type") + } else if length != 1 { + return fmt.Errorf("dhcp: read option length %d, but expected option length for DHCP Message Type is 1", length) + } + + if err := read(&req.DHCPType); err != nil { + return err + } + + return nil +} + +func OptionParameterRequestList(req *BootRequest, r io.Reader) error { + read := newPacketReaderFunc(r) + + var length uint8 + if err := read(&length); err != nil { + return fmt.Errorf("dhcp: reading option length for DHCP Message Type") + } else if length == 0 { + return fmt.Errorf("dhcp: read option length %d, but expected option length for DHCP Parameter Request is >= 1", length) + } + + var parameters = make([]byte, int(length)) + if n, err := r.Read(parameters); err != nil { + return fmt.Errorf("dhcp: while reading parameters: %w", err) + } else if n != int(length) { + return fmt.Errorf("dhcp: only read %d octets of requested parameters, expected %d octets", n, length) + } + + for _, parameter := range parameters { + opt := OptionTag(parameter) + log.Debugf("client is requesting %s", opt) + req.ParameterRequests = append(req.ParameterRequests, opt) + } + + return nil +} + +func OptionEnd(req *BootRequest, r io.Reader) error { + req.endOptions = true + return nil +} + +func ReadOption(req *BootRequest, tag byte, r io.Reader) error { + opt := OptionTag(tag) + if f, ok := optionRegistry[opt]; ok { + log.Debugf("dhcp: reading option=%s", opt) + return f(req, r) + } + + return fmt.Errorf("dhcp: unknown/unhandled option %d", opt) +} + +const magicCookieLength = 4 + +var magicCookie = []byte{99, 130, 83, 99} + +func ReadMagicCookie(r io.Reader) error { + var cookie = make([]byte, magicCookieLength) + n, err := r.Read(cookie) + if err != nil { + return fmt.Errorf("dhcp: failed to read magic cookie: %w", err) + } else if n != magicCookieLength { + return fmt.Errorf("dhcp: read %d bytes, expected to read %d bytes for the magic cookie", + n, magicCookieLength) + } + + if !bytes.Equal(cookie, magicCookie) { + return fmt.Errorf("dhcp: read magic cookie %x, expected %x", cookie, magicCookie) + } + + return nil +} diff --git a/iptools/lease_info.go b/iptools/lease_info.go index fe4e725..a0c941c 100644 --- a/iptools/lease_info.go +++ b/iptools/lease_info.go @@ -15,7 +15,7 @@ type LeaseInfo struct { Expires time.Time `yaml:"expires"` } -type sortableLease []LeaseInfo +type sortableLease []*LeaseInfo func (a sortableLease) Len() int { return len(a) } func (a sortableLease) Swap(i, j int) { a[i], a[j] = a[j], a[i] } @@ -33,14 +33,14 @@ func (li *LeaseInfo) Expire() { li.Expires = time.Time{} } -func SortLeases(leases []LeaseInfo) []LeaseInfo { +func SortLeases(leases []*LeaseInfo) []*LeaseInfo { sortable := sortableLease(leases) sort.Sort(sortable) - return []LeaseInfo(sortable) + return []*LeaseInfo(sortable) } -func (lease LeaseInfo) Reset() LeaseInfo { +func (lease *LeaseInfo) Reset() *LeaseInfo { lease.Expires = time.Time{} lease.HardwareAddress = nil return lease diff --git a/iptools/pool.go b/iptools/pool.go index 0c0db89..94b2f99 100644 --- a/iptools/pool.go +++ b/iptools/pool.go @@ -1,58 +1,141 @@ package iptools import ( + "fmt" "net/netip" + "sync" "time" + + "git.wntrmute.dev/kyle/goutils/assert" ) +const DefaultExpiry = 168 * time.Hour + type Pool struct { - Name string `yaml:"name"` - Range *Range `yaml:"range"` - Expiry time.Duration `yaml:"expiry"` - Available []LeaseInfo `yaml:"available"` - Active map[netip.Addr]LeaseInfo `yaml:"active"` - Limbo map[netip.Addr]LeaseInfo `yaml:"limbo"` // leases that are currently being offered + Name string `yaml:"name"` + Range *Range `yaml:"range"` + Expiry time.Duration `yaml:"expiry"` + Available []*LeaseInfo `yaml:"available"` + Active map[netip.Addr]*LeaseInfo `yaml:"active"` + Limbo map[netip.Addr]*LeaseInfo `yaml:"limbo"` // leases that are currently being offered + NoHostName bool `yaml:"no_hostname"` // don't set the hostname + mtx *sync.Mutex } -func NewPool(name string, exp time.Duration, r *Range) (*Pool, error) { +func (p *Pool) lock() { + if p.lock == nil { + p.mtx = &sync.Mutex{} + } + + p.mtx.Lock() +} + +func (p *Pool) unlock() { + if p.mtx == nil { + panic("pool: attempted to unlock uninitialized mutex") + } + + p.mtx.Unlock() +} + +func NewPool(name string, r *Range) (*Pool, error) { + if r.Expiry == 0 { + r.Expiry = DefaultExpiry + } + p := &Pool{ - Name: name, - Expiry: exp, - Available: enumerateRange(name, r, true), + Name: name, + Expiry: r.Expiry, + NoHostName: r.NoHostName, + Available: enumerateRange(name, r, true), } return p, nil } -func (p *Pool) Sort() { +func (p *Pool) sort() { p.Available = SortLeases(p.Available) } +func (p *Pool) Sort() { + p.lock() + defer p.unlock() + + p.sort() +} + func (p *Pool) IsAddressAvailable() bool { return len(p.Available) > 0 } -func (p *Pool) Peek(t time.Time, waitFor time.Duration) netip.Addr { +// Peek returns the first available address from the pool and moves it +// from available to limbo. When the client is given the address, Accept +// should be called on the address to move it from limbo to active. +func (p *Pool) Peek(t time.Time, waitFor time.Duration) *LeaseInfo { + p.lock() + defer p.unlock() + + li := p.peek(t, waitFor) + return li +} + +func (p *Pool) peek(t time.Time, waitFor time.Duration) *LeaseInfo { + if len(p.Available) == 0 { + return nil + } + lease := p.Available[0] p.Available = p.Available[1:] lease.ResetExpiry(t, waitFor) p.Limbo[lease.Addr] = lease - return lease.Addr + return lease +} + +func (p *Pool) accept(addr netip.Addr) error { + assert.Bool(p.Active[addr] == nil, fmt.Sprintf("limbo address %s is already active: %#v", addr, *p.Active[addr])) + + p.Active[addr] = p.Limbo[addr] + delete(p.Limbo, addr) + return nil +} + +// Accept will move an address out limbo and mark it as active. +func (p *Pool) Accept(addr netip.Addr, t time.Time) error { + p.lock() + defer p.unlock() + + p.update(t) + p.accept(addr) + + return nil } // Update checks expirations on leases, moving any expired leases back // to the available pool and removing any limbo leases that are expired. func (p *Pool) Update(t time.Time) { + p.lock() + defer p.unlock() + + p.update(t) +} + +func (p *Pool) update(t time.Time) { + evictedHosts := []netip.Addr{} + for k, v := range p.Active { if v.IsExpired(t) { - delete(p.Active, k) + evictedHosts = append(evictedHosts, k) v = v.Reset() p.Available = append(p.Available, v) } } + for _, ip := range evictedHosts { + delete(p.Active, ip) + } + for k, v := range p.Limbo { if v.IsExpired(t) { delete(p.Limbo, k) diff --git a/iptools/pool_test.go b/iptools/pool_test.go index 7aa9d77..d15ce3a 100644 --- a/iptools/pool_test.go +++ b/iptools/pool_test.go @@ -4,7 +4,6 @@ import ( "fmt" "net/netip" "testing" - "time" ) var ( @@ -18,7 +17,7 @@ func TestBasicPool(t *testing.T) { End: poolTestIP2, } - p, err := NewPool("cluster", 24*time.Hour, r) + p, err := NewPool("cluster", r) if err != nil { t.Fatal(err) } diff --git a/iptools/range.go b/iptools/range.go index 98978fa..50cfec4 100644 --- a/iptools/range.go +++ b/iptools/range.go @@ -3,6 +3,7 @@ package iptools import ( "fmt" "net/netip" + "time" ) const ( @@ -10,9 +11,11 @@ const ( ) type Range struct { - Start netip.Addr `yaml:"start"` - End netip.Addr `yaml:"end"` - Network netip.Prefix `yaml:"network"` + Start netip.Addr `yaml:"start"` + End netip.Addr `yaml:"end"` + Network netip.Prefix `yaml:"network"` + Expiry time.Duration `yaml:"expiry"` + NoHostName bool `yaml:"no_hostname"` // don't set the hostname } func (r *Range) Validate() error { @@ -54,7 +57,7 @@ func (r *Range) numHosts() int { hosts := 0 for cur.Compare(r.End) < 1 { hosts++ - cur.Next() + cur = cur.Next() } return hosts diff --git a/iptools/range_test.go b/iptools/range_test.go index a62f146..9536116 100644 --- a/iptools/range_test.go +++ b/iptools/range_test.go @@ -47,3 +47,16 @@ func TestBasicValidation(t *testing.T) { t.Fatal("range 4 should be invalid") } } + +func TestNumHosts(t *testing.T) { + r := &Range{ + Start: rangeTestIP1, + End: rangeTestIP2, + } + expected := 15 + + numHosts := r.numHosts() + if numHosts != expected { + t.Fatalf("have %d hosts, expected %d", numHosts, expected) + } +} diff --git a/iptools/tools.go b/iptools/tools.go index 5056a7c..0a4c1f1 100644 --- a/iptools/tools.go +++ b/iptools/tools.go @@ -1,8 +1,10 @@ package iptools -import "fmt" +import ( + "fmt" +) -func enumerateRange(name string, r *Range, startFromOne bool) []LeaseInfo { +func enumerateRange(name string, r *Range, startFromOne bool) []*LeaseInfo { start := r.Start cur := start lenfmt := fmt.Sprintf("%%s%%0%dd", len(fmt.Sprintf("%d", r.numHosts()))) @@ -10,11 +12,12 @@ func enumerateRange(name string, r *Range, startFromOne bool) []LeaseInfo { if startFromOne { i++ } - leases := []LeaseInfo{} + leases := []*LeaseInfo{} for r.End.Compare(cur) >= 0 { - leases = append(leases, LeaseInfo{ - HostName: fmt.Sprintf(lenfmt, name, i), + hostName := fmt.Sprintf(lenfmt, name, i) + leases = append(leases, &LeaseInfo{ + HostName: hostName, Addr: cur, }) i++ diff --git a/kdhcpd.yaml b/kdhcpd.yaml index adf1313..f04ceb8 100644 --- a/kdhcpd.yaml +++ b/kdhcpd.yaml @@ -1,7 +1,7 @@ kdhcp: version: 1 lease_file: /tmp/kdhcp_lease.yaml - interface: deveth0 + interface: enp89s0 port: 67 network: address: 192.168.4.250 @@ -22,6 +22,7 @@ kdhcp: default: start: 192.168.4.128 end: 192.168.4.164 + no_hostname: True statics: controller: 192.168.4.254 haven: 192.168.4.253 diff --git a/server/BUILD.bazel b/server/BUILD.bazel index 21fa194..65af305 100644 --- a/server/BUILD.bazel +++ b/server/BUILD.bazel @@ -5,6 +5,8 @@ go_library( srcs = [ "ifi.go", "ifi_linux.go", + "lease_state.go", + "pools.go", "server.go", ], importpath = "git.wntrmute.dev/kyle/kdhcp/server", @@ -12,6 +14,8 @@ go_library( deps = [ "//config", "//dhcp", + "//iptools", "@dev_wntrmute_git_kyle_goutils//syslog", + "@in_gopkg_yaml_v2//:yaml_v2", ], ) diff --git a/server/lease_state.go b/server/lease_state.go new file mode 100644 index 0000000..501912b --- /dev/null +++ b/server/lease_state.go @@ -0,0 +1,62 @@ +package server + +import ( + "fmt" + "os" + + log "git.wntrmute.dev/kyle/goutils/syslog" + "git.wntrmute.dev/kyle/kdhcp/iptools" + "gopkg.in/yaml.v2" +) + +type LeaseState struct { + Pools map[string]*iptools.Pool `yaml:"pools"` + Static map[string]*iptools.LeaseInfo `yaml:"static"` +} + +func (srv *Server) SaveLeases() error { + leaseFile, err := os.Create(srv.Config.LeaseFile) + if err != nil { + return fmt.Errorf("server: while saving leases: %w", err) + } + defer leaseFile.Close() + encoder := yaml.NewEncoder(leaseFile) + + state := &LeaseState{ + Pools: srv.Pools, + Static: srv.Static, + } + + if err = encoder.Encode(state); err != nil { + return fmt.Errorf("server: while saving leases: %w", err) + } + + if err = encoder.Close(); err != nil { + return fmt.Errorf("server: while saving leases: %w", err) + } + + return nil +} + +func (srv *Server) LoadLeases() error { + leaseState := &LeaseState{} + leaseFile, err := os.Open(srv.Config.LeaseFile) + if err != nil { + if os.IsNotExist(err) { + log.Warningf("server: not loading leases from %s: lease file not found", srv.Config.LeaseFile) + return nil + } + + return fmt.Errorf("server: while reading leases: %w", err) + } + defer leaseFile.Close() + decoder := yaml.NewDecoder(leaseFile) + + if err = decoder.Decode(leaseState); err != nil { + return fmt.Errorf("server: while reading leases: %w", err) + } + + srv.Pools = leaseState.Pools + srv.Static = leaseState.Static + return nil +} diff --git a/server/pools.go b/server/pools.go new file mode 100644 index 0000000..814adfa --- /dev/null +++ b/server/pools.go @@ -0,0 +1,37 @@ +package server + +import ( + "fmt" + "net/netip" + + log "git.wntrmute.dev/kyle/goutils/syslog" + "git.wntrmute.dev/kyle/kdhcp/iptools" +) + +// pools.go adds pool functionality to the server. + +func (srv *Server) loadPoolsFromConfig() error { + for host, ip := range srv.Config.Statics { + addr, ok := netip.AddrFromSlice(ip.To4()) + if !ok { + return fmt.Errorf("server: while instantiating pools, could not load IP %s", ip) + } + log.Debugf("server: added static host entry %s -> %s", host, addr) + srv.Static[host] = &iptools.LeaseInfo{ + HostName: host, + Addr: addr, + } + } + + for name, ipRange := range srv.Config.Pools { + pool, err := iptools.NewPool(name, ipRange) + if err != nil { + return fmt.Errorf("server: couldn't load pool %s: %w", name, err) + } + log.Debugf("server: added pool %s: %s -> %s", name, ipRange.Start, ipRange.End) + + srv.Pools[name] = pool + } + + return nil +} diff --git a/server/server.go b/server/server.go index 1440e8f..831a251 100644 --- a/server/server.go +++ b/server/server.go @@ -3,19 +3,25 @@ package server import ( "errors" "net" + "time" log "git.wntrmute.dev/kyle/goutils/syslog" "git.wntrmute.dev/kyle/kdhcp/config" "git.wntrmute.dev/kyle/kdhcp/dhcp" + "git.wntrmute.dev/kyle/kdhcp/iptools" ) const ( - MaxPacketSize = 512 + DefaultPool = "default" + MaxPacketSize = 512 + MaxResponseWait = 5 * time.Minute ) type Server struct { Conn net.PacketConn Config *config.Config + Pools map[string]*iptools.Pool + Static map[string]*iptools.LeaseInfo } func (s *Server) Close() error { @@ -24,6 +30,8 @@ func (s *Server) Close() error { func (s *Server) Bind() (err error) { // In order to read DHCP packets, we'll need to listen on all addresses. + // That being said, we also want to limit our listening to the DHCP + // network device. ip := net.IP([]byte{0, 0, 0, 0}) s.Conn, err = BindInterface(ip, s.Config.Port, s.Config.Interface) return err @@ -46,7 +54,7 @@ func (s *Server) ReadDHCPRequest() (*dhcp.BootRequest, error) { return nil, err } - log.Debugf("server: read %db packet from %s", len(pkt), addr) + log.Debugf("server: read packet from %s", addr) return dhcp.ReadRequest(pkt) } @@ -77,10 +85,19 @@ func (s *Server) Listen() { func New(cfg *config.Config) (*Server, error) { srv := &Server{ Config: cfg, + Pools: map[string]*iptools.Pool{}, + Static: map[string]*iptools.LeaseInfo{}, } - err := srv.Bind() - if err != nil { + if err := srv.loadPoolsFromConfig(); err != nil { + return nil, err + } + + if err := srv.LoadLeases(); err != nil { + return nil, err + } + + if err := srv.Bind(); err != nil { return nil, err } @@ -88,3 +105,19 @@ func New(cfg *config.Config) (*Server, error) { return srv, nil } + +func (srv *Server) SelectLease(req *dhcp.BootRequest, t time.Time) *iptools.LeaseInfo { + if li, ok := srv.Static[req.HostName]; ok { + return li + } + + if pool, ok := srv.Pools[req.HostName]; ok { + return pool.Peek(t, MaxResponseWait) + } + + if pool, ok := srv.Pools[DefaultPool]; ok { + return pool.Peek(t, MaxResponseWait) + } + + return nil +}