290 lines
6.3 KiB
Go
290 lines
6.3 KiB
Go
// Copyright 2016 The Netstack Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package dhcp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"gvisor.googlesource.com/gvisor/pkg/tcpip"
|
|
"gvisor.googlesource.com/gvisor/pkg/tcpip/network/ipv4"
|
|
"gvisor.googlesource.com/gvisor/pkg/tcpip/stack"
|
|
"gvisor.googlesource.com/gvisor/pkg/tcpip/transport/udp"
|
|
"gvisor.googlesource.com/gvisor/pkg/waiter"
|
|
)
|
|
|
|
// Server is a DHCP server.
|
|
type Server struct {
|
|
stack *stack.Stack
|
|
broadcast tcpip.FullAddress
|
|
wq waiter.Queue
|
|
ep tcpip.Endpoint
|
|
addrs []tcpip.Address // TODO: use a tcpip.AddressMask or range structure
|
|
cfg Config
|
|
cfgopts []option // cfg to send to client
|
|
|
|
handlers []chan header
|
|
|
|
mu sync.Mutex
|
|
leases map[tcpip.LinkAddress]serverLease
|
|
}
|
|
|
|
// NewServer creates a new DHCP server and begins serving.
|
|
// The server continues serving until ctx is done.
|
|
func NewServer(ctx context.Context, stack *stack.Stack, addrs []tcpip.Address, cfg Config) (*Server, error) {
|
|
s := &Server{
|
|
stack: stack,
|
|
addrs: addrs,
|
|
cfg: cfg,
|
|
cfgopts: cfg.encode(),
|
|
broadcast: tcpip.FullAddress{
|
|
Addr: "\xff\xff\xff\xff",
|
|
Port: clientPort,
|
|
},
|
|
|
|
handlers: make([]chan header, 8),
|
|
leases: make(map[tcpip.LinkAddress]serverLease),
|
|
}
|
|
|
|
var err *tcpip.Error
|
|
s.ep, err = s.stack.NewEndpoint(udp.ProtocolNumber, ipv4.ProtocolNumber, &s.wq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dhcp: server endpoint: %v", err)
|
|
}
|
|
serverBroadcast := tcpip.FullAddress{
|
|
Addr: "",
|
|
Port: serverPort,
|
|
}
|
|
if err := s.ep.Bind(serverBroadcast, nil); err != nil {
|
|
return nil, fmt.Errorf("dhcp: server bind: %v", err)
|
|
}
|
|
|
|
for i := 0; i < len(s.handlers); i++ {
|
|
ch := make(chan header, 8)
|
|
s.handlers[i] = ch
|
|
go s.handler(ctx, ch)
|
|
}
|
|
|
|
go s.expirer(ctx)
|
|
go s.reader(ctx)
|
|
return s, nil
|
|
}
|
|
|
|
func (s *Server) expirer(ctx context.Context) {
|
|
t := time.NewTicker(1 * time.Minute)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
case <-t.C:
|
|
s.mu.Lock()
|
|
for linkAddr, lease := range s.leases {
|
|
if time.Since(lease.start) > s.cfg.LeaseLength {
|
|
lease.state = leaseExpired
|
|
s.leases[linkAddr] = lease
|
|
}
|
|
}
|
|
s.mu.Unlock()
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// reader listens for all incoming DHCP packets and fans them out to
|
|
// handling goroutines based on XID as session identifiers.
|
|
func (s *Server) reader(ctx context.Context) {
|
|
we, ch := waiter.NewChannelEntry(nil)
|
|
s.wq.EventRegister(&we, waiter.EventIn)
|
|
defer s.wq.EventUnregister(&we)
|
|
|
|
for {
|
|
var addr tcpip.FullAddress
|
|
v, _, err := s.ep.Read(&addr)
|
|
if err == tcpip.ErrWouldBlock {
|
|
select {
|
|
case <-ch:
|
|
continue
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
|
|
h := header(v)
|
|
if !h.isValid() || h.op() != opRequest {
|
|
continue
|
|
}
|
|
xid := h.xid()
|
|
|
|
// Fan out the packet to a handler goroutine.
|
|
//
|
|
// Use a consistent handler for a given xid, so that
|
|
// packets from a particular client are processed
|
|
// in order.
|
|
ch := s.handlers[int(xid)%len(s.handlers)]
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case ch <- h:
|
|
default:
|
|
// drop the packet
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) handler(ctx context.Context, ch chan header) {
|
|
for {
|
|
select {
|
|
case h := <-ch:
|
|
if h == nil {
|
|
return
|
|
}
|
|
opts, err := h.options()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
// TODO: Handle DHCPRELEASE and DHCPDECLINE.
|
|
msgtype, err := opts.dhcpMsgType()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
switch msgtype {
|
|
case dhcpDISCOVER:
|
|
s.handleDiscover(h, opts)
|
|
case dhcpREQUEST:
|
|
s.handleRequest(h, opts)
|
|
}
|
|
case <-ctx.Done():
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleDiscover(hreq header, opts options) {
|
|
linkAddr := tcpip.LinkAddress(hreq.chaddr()[:6])
|
|
xid := hreq.xid()
|
|
|
|
s.mu.Lock()
|
|
lease := s.leases[linkAddr]
|
|
switch lease.state {
|
|
case leaseNew:
|
|
if len(s.leases) < len(s.addrs) {
|
|
// Find an unused address.
|
|
// TODO: avoid building this state on each request.
|
|
alloced := make(map[tcpip.Address]bool)
|
|
for _, lease := range s.leases {
|
|
alloced[lease.addr] = true
|
|
}
|
|
for _, addr := range s.addrs {
|
|
if !alloced[addr] {
|
|
lease = serverLease{
|
|
start: time.Now(),
|
|
addr: addr,
|
|
xid: xid,
|
|
state: leaseOffer,
|
|
}
|
|
s.leases[linkAddr] = lease
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
// No more addresses, take an expired address.
|
|
for k, oldLease := range s.leases {
|
|
if oldLease.state == leaseExpired {
|
|
delete(s.leases, k)
|
|
lease = serverLease{
|
|
start: time.Now(),
|
|
addr: lease.addr,
|
|
xid: xid,
|
|
state: leaseOffer,
|
|
}
|
|
s.leases[linkAddr] = lease
|
|
break
|
|
}
|
|
}
|
|
log.Printf("server has no more addresses")
|
|
s.mu.Unlock()
|
|
return
|
|
}
|
|
case leaseOffer, leaseAck, leaseExpired:
|
|
lease = serverLease{
|
|
start: time.Now(),
|
|
addr: s.leases[linkAddr].addr,
|
|
xid: xid,
|
|
state: leaseOffer,
|
|
}
|
|
s.leases[linkAddr] = lease
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
// DHCPOFFER
|
|
opts = options{{optDHCPMsgType, []byte{byte(dhcpOFFER)}}}
|
|
opts = append(opts, s.cfgopts...)
|
|
h := make(header, headerBaseSize+opts.len())
|
|
h.init()
|
|
h.setOp(opReply)
|
|
copy(h.xidbytes(), hreq.xidbytes())
|
|
copy(h.yiaddr(), lease.addr)
|
|
copy(h.siaddr(), s.cfg.ServerAddress)
|
|
copy(h.chaddr(), hreq.chaddr())
|
|
h.setOptions(opts)
|
|
s.ep.Write(tcpip.SlicePayload(h), tcpip.WriteOptions{To: &s.broadcast})
|
|
}
|
|
|
|
func (s *Server) handleRequest(hreq header, opts options) {
|
|
linkAddr := tcpip.LinkAddress(hreq.chaddr()[:6])
|
|
xid := hreq.xid()
|
|
|
|
s.mu.Lock()
|
|
lease := s.leases[linkAddr]
|
|
switch lease.state {
|
|
case leaseOffer, leaseAck, leaseExpired:
|
|
lease = serverLease{
|
|
start: time.Now(),
|
|
addr: s.leases[linkAddr].addr,
|
|
xid: xid,
|
|
state: leaseAck,
|
|
}
|
|
s.leases[linkAddr] = lease
|
|
}
|
|
s.mu.Unlock()
|
|
|
|
if lease.state == leaseNew {
|
|
// TODO: NACK or accept request
|
|
return
|
|
}
|
|
|
|
// DHCPACK
|
|
opts = []option{{optDHCPMsgType, []byte{byte(dhcpACK)}}}
|
|
opts = append(opts, s.cfgopts...)
|
|
h := make(header, headerBaseSize+opts.len())
|
|
h.init()
|
|
h.setOp(opReply)
|
|
copy(h.xidbytes(), hreq.xidbytes())
|
|
copy(h.yiaddr(), lease.addr)
|
|
copy(h.siaddr(), s.cfg.ServerAddress)
|
|
copy(h.chaddr(), hreq.chaddr())
|
|
h.setOptions(opts)
|
|
s.ep.Write(tcpip.SlicePayload(h), tcpip.WriteOptions{To: &s.broadcast})
|
|
}
|
|
|
|
type leaseState int
|
|
|
|
const (
|
|
leaseNew leaseState = iota
|
|
leaseOffer
|
|
leaseAck
|
|
leaseExpired
|
|
)
|
|
|
|
type serverLease struct {
|
|
start time.Time
|
|
addr tcpip.Address
|
|
xid xid
|
|
state leaseState
|
|
}
|