dhcp.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package pxe
  15. import (
  16. "context"
  17. "encoding/hex"
  18. "fmt"
  19. "net"
  20. "strings"
  21. "yunion.io/x/jsonutils"
  22. "yunion.io/x/log"
  23. "yunion.io/x/pkg/errors"
  24. "yunion.io/x/pkg/util/printutils"
  25. "yunion.io/x/onecloud/pkg/apis"
  26. api "yunion.io/x/onecloud/pkg/apis/compute"
  27. o "yunion.io/x/onecloud/pkg/baremetal/options"
  28. "yunion.io/x/onecloud/pkg/cloudcommon/types"
  29. "yunion.io/x/onecloud/pkg/httperrors"
  30. "yunion.io/x/onecloud/pkg/mcclient"
  31. modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
  32. "yunion.io/x/onecloud/pkg/util/ctx"
  33. "yunion.io/x/onecloud/pkg/util/dhcp"
  34. )
  35. func (s *Server) serveDHCP(srv *dhcp.DHCPServer, handler dhcp.DHCPHandler) error {
  36. return srv.ListenAndServe(handler)
  37. }
  38. type NetworkInterfaceIdent struct {
  39. Type uint16
  40. Major uint16
  41. Minior uint16
  42. }
  43. type DHCPHandler struct {
  44. // baremetal manager
  45. baremetalManager IBaremetalManager
  46. }
  47. type dhcpRequest struct {
  48. packet dhcp.Packet
  49. ClientMac net.HardwareAddr // client nic mac
  50. ClientAddr net.IP // IP address from DHCP client
  51. RelayAddr net.IP // IP address of DHCP relay agent
  52. Options dhcp.Options // dhcp packet options
  53. VendorClassId string
  54. ClientArch uint16
  55. NetworkInterfaceIdent NetworkInterfaceIdent
  56. ClientGuid string
  57. // baremetal manager
  58. baremetalManager IBaremetalManager
  59. // baremetal instance
  60. baremetalInstance IBaremetalInstance
  61. // cloud network config
  62. netConfig *types.SNetworkConfig
  63. }
  64. func (h *DHCPHandler) ServeDHCP(pkt dhcp.Packet, _ net.HardwareAddr, _ *net.UDPAddr) (dhcp.Packet, []string, error) {
  65. req, err := h.newRequest(pkt, h.baremetalManager)
  66. if err != nil {
  67. log.Errorf("[DHCP] new request by packet error: %v", err)
  68. return nil, nil, err
  69. }
  70. log.V(4).Debugf("[DHCP] request packet: %#v", req)
  71. if req.RelayAddr.String() == "0.0.0.0" {
  72. return nil, nil, fmt.Errorf("Request not from a DHCP relay, ignore mac: %s", req.ClientMac)
  73. }
  74. log.Infof("[DHCP] from relay %s packet, mac: %s", req.RelayAddr, req.ClientMac)
  75. conf, targets, err := req.fetchConfig(ctx.CtxWithTime(), h.baremetalManager.GetClientSession())
  76. if err != nil {
  77. return nil, nil, errors.Wrapf(err, "fetchConfig for %s", req.ClientMac.String())
  78. }
  79. if conf == nil {
  80. return nil, nil, fmt.Errorf("Empty packet config")
  81. }
  82. // handle Option 82 relay information
  83. // https://datatracker.ietf.org/doc/html/rfc3046
  84. if relayInfo := pkt.GetOptionValue(dhcp.OptionRelayAgentInformation); relayInfo != nil {
  85. // return relay information transparently
  86. conf.RelayInfo = relayInfo
  87. }
  88. pkg, err := dhcp.MakeReplyPacket(pkt, conf)
  89. if err != nil {
  90. return nil, nil, errors.Wrap(err, "dhcp.MakeReplyPacket")
  91. }
  92. return pkg, targets, nil
  93. }
  94. func (h *DHCPHandler) newRequest(pkt dhcp.Packet, man IBaremetalManager) (*dhcpRequest, error) {
  95. req := &dhcpRequest{
  96. baremetalManager: man,
  97. packet: pkt,
  98. ClientAddr: pkt.CIAddr(),
  99. ClientMac: pkt.CHAddr(),
  100. RelayAddr: pkt.RelayAddr(),
  101. Options: pkt.ParseOptions(),
  102. }
  103. var (
  104. vendorClsId string
  105. cliArch uint16
  106. err error
  107. netIfIdent NetworkInterfaceIdent
  108. cliGuid string
  109. )
  110. for optCode, data := range req.Options {
  111. switch optCode {
  112. case dhcp.OptionVendorClassIdentifier:
  113. vendorClsId, err = req.Options.String(optCode)
  114. case dhcp.OptionClientArchitecture:
  115. cliArch, err = req.Options.Uint16(optCode)
  116. case dhcp.OptionClientNetworkInterfaceIdentifier:
  117. netIfIdentBs, err := req.Options.Bytes(optCode)
  118. if err != nil {
  119. break
  120. }
  121. netIfIdent = NetworkInterfaceIdent{
  122. Type: uint16(netIfIdentBs[0]),
  123. Major: uint16(netIfIdentBs[1]),
  124. Minior: uint16(netIfIdentBs[2]),
  125. }
  126. log.Debugf("[DHCP] get network iface identifier: %#v", netIfIdent)
  127. case dhcp.OptionClientMachineIdentifier:
  128. switch len(data) {
  129. case 0:
  130. // A missing GUID is invalid according to the spec, however
  131. // there are PXE ROMs in the wild that omit the GUID and still
  132. // expect to boot.
  133. case 17:
  134. if data[0] != 0 {
  135. err = errors.Error("malformed client GUID (option 97), leading byte must be zero")
  136. }
  137. default:
  138. err = errors.Error("malformed client GUID (option 97), wrong size")
  139. }
  140. cliGuid, err = req.Options.String(optCode)
  141. }
  142. if err != nil {
  143. log.Errorf("[DHCP] parse vendor option %d error: %v", optCode, err)
  144. }
  145. }
  146. var cliUUIDStr string
  147. if len(cliGuid) == 17 {
  148. cliUUIDStr = formatUuidString([]byte(cliGuid)[1:])
  149. }
  150. req.VendorClassId = vendorClsId
  151. req.ClientArch = cliArch
  152. req.NetworkInterfaceIdent = netIfIdent
  153. req.ClientGuid = cliUUIDStr
  154. log.Debugf("Client GUID: %s", req.ClientGuid)
  155. return req, err
  156. }
  157. func swapBytes(input []byte) []byte {
  158. output := make([]byte, len(input))
  159. for i := range input {
  160. output[i] = input[len(input)-1-i]
  161. }
  162. return output
  163. }
  164. func formatUuidString(uuidBytes []byte) string {
  165. return strings.Join([]string{
  166. hex.EncodeToString(swapBytes(uuidBytes[0:4])),
  167. hex.EncodeToString(swapBytes(uuidBytes[4:6])),
  168. hex.EncodeToString(swapBytes(uuidBytes[6:8])),
  169. hex.EncodeToString(uuidBytes[8:10]),
  170. hex.EncodeToString(uuidBytes[10:16]),
  171. }, "-")
  172. }
  173. func (req *dhcpRequest) fetchConfig(ctx context.Context, session *mcclient.ClientSession) (*dhcp.ResponseConfig, []string, error) {
  174. // 1. find_network_conf
  175. netConf, err := req.findNetworkConf(session, false)
  176. if err != nil {
  177. return nil, nil, err
  178. }
  179. req.netConfig = netConf
  180. var dhcpAddrList []string
  181. if len(netConf.GuestDhcp) > 0 {
  182. dhcpAddrList = strings.Split(netConf.GuestDhcp, ",")
  183. }
  184. log.Debugf("find network config %#v, dhcpAddrList %s", netConf, dhcpAddrList)
  185. // TODO: set cache for netConf
  186. if req.isPXERequest() {
  187. if !o.Options.EnablePxeBoot {
  188. return nil, nil, errors.Error("PXE Boot disabled")
  189. }
  190. // handle PXE DHCP request
  191. log.Infof("[PXE DHCP] relay from %s(%s) for %s, find matched networks: %#v", req.RelayAddr, req.ClientAddr, req.ClientMac, netConf)
  192. bmDesc, err := req.createOrUpdateBaremetal(session)
  193. if err != nil {
  194. return nil, nil, errors.Wrapf(err, "createOrUpdateBaremetal for %s", req.ClientMac.String())
  195. }
  196. err = req.doInitBaremetalAdminNetif(ctx, bmDesc)
  197. if err != nil {
  198. return nil, nil, errors.Wrapf(err, "doInitBaremetalAdminNetif for %s", req.ClientMac.String())
  199. }
  200. // always response PXE request
  201. // let bootloader decide boot local or remote
  202. // if req.baremetalInstance.NeedPXEBoot() {
  203. conf, err := req.baremetalInstance.GetPXEDHCPConfig(req.ClientArch)
  204. if err != nil {
  205. return nil, nil, errors.Wrapf(err, "req.baremetalInstance.GetPXEDHCPConfig for %s", req.ClientMac.String())
  206. }
  207. return conf, dhcpAddrList, nil
  208. // }
  209. // ignore
  210. // log.Warningf("No need to pxeboot, ignore the request ...(mac:%s guid:%s)", req.ClientMac, req.ClientGuid)
  211. // return nil, nil
  212. } else {
  213. // handle normal DHCP request
  214. bmInstance := req.baremetalManager.GetBaremetalByMac(req.ClientMac)
  215. if bmInstance == nil {
  216. // options.EnableGeneralGuestDhcp
  217. // cloud be an instance not served by a host-server
  218. // from guestdhcp import GuestDHCPHelperTask
  219. // task = GuestDHCPHelperTask(self)
  220. // task.start()
  221. log.Infof("[NORMAL DHCP] Not found baremetal from request with mac: %s", req.ClientMac)
  222. return nil, nil, nil
  223. }
  224. req.baremetalInstance = bmInstance
  225. ipmiNic := req.baremetalInstance.GetIPMINic(req.ClientMac)
  226. if ipmiNic != nil && ipmiNic.Mac == req.ClientMac.String() {
  227. err = req.baremetalInstance.InitAdminNetif(ctx,
  228. req.ClientMac, req.netConfig.WireId, api.NIC_TYPE_IPMI, api.NETWORK_TYPE_IPMI, false, "")
  229. if err != nil {
  230. return nil, nil, errors.Wrapf(err, "InitAdminNetif for %s", req.ClientMac.String())
  231. }
  232. } else {
  233. err = req.baremetalInstance.RegisterNetif(ctx, req.ClientMac, req.netConfig.WireId)
  234. if err != nil {
  235. log.Errorf("RegisterNetif %s error: %v", req.ClientMac.String(), err)
  236. return nil, nil, errors.Wrapf(err, "RegisterNetif for %s", req.ClientMac.String())
  237. }
  238. }
  239. conf, err := req.baremetalInstance.GetDHCPConfig(req.ClientMac)
  240. if err != nil {
  241. return nil, nil, errors.Wrapf(err, "req.baremetalInstance.GetDHCPConfig for %s", req.ClientMac.String())
  242. }
  243. return conf, dhcpAddrList, nil
  244. }
  245. }
  246. func (req *dhcpRequest) findNetworkConf(session *mcclient.ClientSession, filterUseIp bool) (*types.SNetworkConfig, error) {
  247. params := jsonutils.NewDict()
  248. if filterUseIp {
  249. params.Add(jsonutils.NewString(req.RelayAddr.String()), "ip")
  250. } else {
  251. params.Add(jsonutils.NewString(
  252. fmt.Sprintf("guest_gateway.equals(%s)", req.RelayAddr)),
  253. "filter.0")
  254. params.Add(jsonutils.NewString(
  255. fmt.Sprintf("guest_dhcp.startswith('%s,')", req.RelayAddr)),
  256. "filter.1")
  257. params.Add(jsonutils.NewString(
  258. fmt.Sprintf("guest_dhcp.contains(',%s,')", req.RelayAddr)),
  259. "filter.2")
  260. params.Add(jsonutils.NewString(
  261. fmt.Sprintf("guest_dhcp.endswith(',%s')", req.RelayAddr)),
  262. "filter.3")
  263. params.Add(jsonutils.NewString(
  264. fmt.Sprintf("guest_dhcp.equals(%s)", req.RelayAddr)),
  265. "filter.4")
  266. params.Add(jsonutils.JSONTrue, "filter_any")
  267. }
  268. params.Add(jsonutils.JSONTrue, "is_classic")
  269. params.Add(jsonutils.NewString("system"), "scope")
  270. ret, err := modules.Networks.List(session, params)
  271. if err != nil {
  272. return nil, err
  273. }
  274. if len(ret.Data) == 0 {
  275. if !filterUseIp {
  276. // use ip filter try again
  277. return req.findNetworkConf(session, true)
  278. }
  279. return nil, fmt.Errorf("DHCP relay from %s(%s) for %s, find no match network", req.RelayAddr, req.ClientAddr, req.ClientMac)
  280. }
  281. idx := 0
  282. for i := range ret.Data {
  283. netType, _ := ret.Data[i].GetString("server_type")
  284. if netType == string(api.NETWORK_TYPE_PXE) {
  285. idx = i
  286. break
  287. }
  288. }
  289. network := types.SNetworkConfig{}
  290. err = ret.Data[idx].Unmarshal(&network)
  291. return &network, err
  292. }
  293. func (req *dhcpRequest) findBaremetalsByUuid(session *mcclient.ClientSession) (*printutils.ListResult, error) {
  294. params := jsonutils.NewDict()
  295. params.Add(jsonutils.NewString(req.ClientGuid), "uuid")
  296. params.Add(jsonutils.NewString("system"), "scope")
  297. ret, err := modules.Hosts.List(session, params)
  298. if err != nil {
  299. return nil, err
  300. }
  301. if len(ret.Data) > 1 {
  302. return nil, httperrors.NewDuplicateResourceError("duplicate uuid %s", req.ClientGuid)
  303. }
  304. return ret, nil
  305. }
  306. func (req *dhcpRequest) findBaremetalsOfAnyMac(session *mcclient.ClientSession, isBaremetal bool) (*printutils.ListResult, error) {
  307. params := jsonutils.NewDict()
  308. params.Add(jsonutils.NewString(req.ClientMac.String()), "any_mac")
  309. params.Add(jsonutils.NewString("system"), "scope")
  310. if isBaremetal {
  311. params.Add(jsonutils.JSONTrue, "is_baremetal")
  312. } else {
  313. params.Add(jsonutils.NewString(api.HOST_TYPE_BAREMETAL), "host_type")
  314. }
  315. return modules.Hosts.List(session, params)
  316. }
  317. // createOrUpdateBaremetal create or update baremetal by client MAC
  318. func (req *dhcpRequest) createOrUpdateBaremetal(session *mcclient.ClientSession) (jsonutils.JSONObject, error) {
  319. // first try mac and is_baremetal=true
  320. ret, err := req.findBaremetalsOfAnyMac(session, true)
  321. if err != nil {
  322. return nil, err
  323. }
  324. if len(ret.Data) == 0 {
  325. // try mac and host_type=baremetal
  326. ret, err = req.findBaremetalsOfAnyMac(session, false)
  327. if err != nil {
  328. return nil, err
  329. }
  330. }
  331. if len(ret.Data) == 0 && len(req.ClientGuid) > 0 {
  332. // try UUID
  333. ret, err = req.findBaremetalsByUuid(session)
  334. if err != nil {
  335. return nil, err
  336. }
  337. }
  338. switch len(ret.Data) {
  339. case 0:
  340. // found new baremetal, create it if auto register
  341. if o.Options.AutoRegisterBaremetal {
  342. return req.createBaremetal(session)
  343. }
  344. case 1:
  345. // already exists, do update
  346. bmId, err := ret.Data[0].GetString("id")
  347. if err != nil {
  348. return nil, err
  349. }
  350. return req.updateBaremetal(session, bmId)
  351. }
  352. return nil, fmt.Errorf("Found %d records match %s", len(ret.Data), req.ClientMac)
  353. }
  354. func (req *dhcpRequest) getArch() string {
  355. switch req.ClientArch {
  356. case dhcp.CLIENT_ARCH_EFI_BC, dhcp.CLIENT_ARCH_EFI_X86_64:
  357. return apis.OS_ARCH_X86_64
  358. case dhcp.CLIENT_ARCH_EFI_IA32:
  359. return apis.OS_ARCH_X86_32
  360. case dhcp.CLIENT_ARCH_EFI_ARM64:
  361. return apis.OS_ARCH_AARCH64
  362. case dhcp.CLIENT_ARCH_EFI_ARM32:
  363. return apis.OS_ARCH_AARCH32
  364. default:
  365. return ""
  366. }
  367. }
  368. func (req *dhcpRequest) createBaremetal(session *mcclient.ClientSession) (jsonutils.JSONObject, error) {
  369. params := jsonutils.NewDict()
  370. mac := req.ClientMac.String()
  371. zoneId := req.baremetalManager.GetZoneId()
  372. name := strings.ToLower(fmt.Sprintf("BM%s", strings.Replace(mac, ":", "", -1)))
  373. params.Add(jsonutils.NewString(name), "name")
  374. params.Add(jsonutils.NewString(mac), "access_mac")
  375. params.Add(jsonutils.NewString("baremetal"), "host_type")
  376. params.Add(jsonutils.JSONTrue, "is_baremetal")
  377. params.Add(jsonutils.NewString(zoneId), "zone_id")
  378. params.Add(jsonutils.NewString(req.getArch()), "cpu_architecture")
  379. desc, err := modules.Hosts.Create(session, params)
  380. if err != nil {
  381. return nil, err
  382. }
  383. return desc, nil
  384. }
  385. func (req *dhcpRequest) updateBaremetal(session *mcclient.ClientSession, id string) (jsonutils.JSONObject, error) {
  386. params := jsonutils.NewDict()
  387. params.Add(jsonutils.NewString(req.ClientMac.String()), "access_mac")
  388. params.Add(jsonutils.NewString(req.baremetalManager.GetZoneId()), "zone_id")
  389. params.Add(jsonutils.NewString(req.getArch()), "cpu_architecture")
  390. // params.Add(jsonutils.NewString("baremetal"), "host_type")
  391. params.Add(jsonutils.JSONTrue, "is_baremetal")
  392. if !dhcp.IsUEFIPxeArch(req.ClientArch) {
  393. // clean uefi info
  394. params.Add(jsonutils.NewDict(), "uefi_info")
  395. }
  396. desc, err := modules.Hosts.Update(session, id, params)
  397. if err != nil {
  398. return nil, err
  399. }
  400. return desc, nil
  401. }
  402. func (req *dhcpRequest) doInitBaremetalAdminNetif(ctx context.Context, desc jsonutils.JSONObject) error {
  403. var err error
  404. req.baremetalInstance, err = req.baremetalManager.AddBaremetal(ctx, desc)
  405. if err != nil {
  406. return err
  407. }
  408. err = req.baremetalInstance.InitAdminNetif(ctx,
  409. req.ClientMac, req.netConfig.WireId, api.NIC_TYPE_ADMIN, api.NETWORK_TYPE_PXE, false, "")
  410. return err
  411. }
  412. func (req *dhcpRequest) isPXERequest() bool {
  413. pkt := req.packet
  414. return dhcp.IsPXERequest(pkt)
  415. }
  416. func (s *Server) validateDHCP(pkt dhcp.Packet) (Machine, Firmware, error) {
  417. var mach Machine
  418. var fwtype Firmware
  419. fwt, err := pkt.ParseOptions().Uint16(dhcp.OptionClientArchitecture)
  420. if err != nil {
  421. return mach, fwtype, fmt.Errorf("malformed DHCP option 93 (required for PXE): %s", err)
  422. }
  423. // Basic architecture and firmware identification, based purely on
  424. // the PXE architecture option.
  425. switch fwt {
  426. // TODO: complete case 1, 2, 3, 4, 5, 8
  427. case 0:
  428. // Intel x86PC
  429. mach.Arch = ArchIA32
  430. fwtype = FirmwareX86PC
  431. case 1:
  432. // NEC/PC98
  433. mach.Arch = ArchUnknown
  434. fwtype = FirmwareUnknown
  435. case 2:
  436. // EFI Itanium
  437. mach.Arch = ArchUnknown
  438. fwtype = FirmwareUnknown
  439. case 3:
  440. // DEC Alpha
  441. mach.Arch = ArchUnknown
  442. fwtype = FirmwareUnknown
  443. case 4:
  444. // Arc x86
  445. mach.Arch = ArchUnknown
  446. fwtype = FirmwareUnknown
  447. case 5:
  448. // Intel Lean Client
  449. mach.Arch = ArchUnknown
  450. fwtype = FirmwareUnknown
  451. case 6:
  452. // EFI IA32
  453. mach.Arch = ArchIA32
  454. fwtype = FirmwareEFI32
  455. case 7:
  456. // EFI BC
  457. mach.Arch = ArchX64
  458. fwtype = FirmwareEFI64
  459. case 8:
  460. // EFI Xscale
  461. mach.Arch = ArchUnknown
  462. fwtype = FirmwareUnknown
  463. case 9:
  464. // EFI x86-64
  465. mach.Arch = ArchX64
  466. fwtype = FirmwareEFIBC
  467. default:
  468. return mach, 0, fmt.Errorf("unsupported client firmware type '%d'", fwtype)
  469. }
  470. guid, _ := pkt.ParseOptions().Bytes(dhcp.OptionClientMachineIdentifier)
  471. switch len(guid) {
  472. case 0:
  473. // A missing GUID is invalid according to the spec, however
  474. // there are PXE ROMs in the wild that omit the GUID and still
  475. // expect to boot. The only thing we do with the GUID is
  476. // mirror it back to the client if it's there, so we might as
  477. // well accept these buggy ROMs.
  478. case 17:
  479. if guid[0] != 0 {
  480. return mach, 0, errors.Error("malformed client GUID (option 97), leading byte must be zero")
  481. }
  482. default:
  483. return mach, 0, errors.Error("malformed client GUID (option 97), wrong size")
  484. }
  485. mach.MAC = pkt.CHAddr()
  486. return mach, fwtype, nil
  487. }