├── ipmisim ├── __init__.py ├── fakebmc.py ├── fakesession.py └── ipmisim.py ├── .gitignore ├── Makefile ├── README.md ├── setup.py └── LICENSE /ipmisim/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *pyc 2 | *egg-info 3 | dist 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | python setup.py sdist 3 | 4 | clean: 5 | rm -fr dist ipmisim.egg-info ipmisim/*pyc 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ipmisim 2 | 3 | A fake ipmi server for testing purposes both as a tool and a library. 4 | The code is forked from [Conpot](http://conpot.org/) and based on `pyghmi`. 5 | 6 | ![version badge](https://badge.fury.io/py/ipmisim.png) ![download badge](http://img.shields.io/pypi/dm/ipmisim.png) 7 | 8 | This was created for testing IPMI related features in [Apache CloudStack](http://cloudstack.apache.org). 9 | 10 | The tool ships with default sets of users for ease of use: 11 | 12 | ID Name Callin Link Auth IPMI Msg Channel Priv Limit 13 | 1 admin true true true ADMINISTRATOR 14 | 2 operator true false false OPERATOR 15 | 3 user true true true USER 16 | 17 | The default passwords are: 18 | 19 | admin : password 20 | opuser : oppassword 21 | user : userpassword 22 | 23 | Installation: 24 | 25 | pip install --upgrade ipmisim 26 | 27 | Try to install `pycryptodome` when having issues with ipmisim, instead of pycrypto. Try this: 28 | ``` 29 | pip uninstall pycrypto 30 | pip install pycryptodome 31 | ``` 32 | 33 | Running: 34 | 35 | ipmisim 3000 # Runs on custom port 3000, else 9001 by default 36 | 37 | For, usage in integration tests you can import the server module and create a server: 38 | 39 | from ipmisim.ipmisim import IpmiServer 40 | import socketserver 41 | 42 | port = 3000 43 | server = SocketServer.UDPServer(('0.0.0.0', port), IpmiServer) 44 | server.serve_forever() 45 | 46 | For testing BMC power state, you can inspect `IpmiServerContext().bmc.powerstate` 47 | For more details see server usage `ipmisim/ipmisim.py` 48 | 49 | Testing with ipmitool: 50 | 51 | ipmitool -I lanplus -H localhost -p 9001 -R1 -U admin -P password chassis power status 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | 18 | try: 19 | from setuptools import setup, find_packages 20 | except ImportError: 21 | from distribute_setup import use_setuptools 22 | use_setuptools() 23 | from setuptools import setup, find_packages 24 | 25 | requires = [ 26 | 'pyghmi==1.2.16', 27 | 'future==0.18.2', 28 | 'pycrypto==2.6.1', 29 | ] 30 | 31 | setup( 32 | name = 'ipmisim', 33 | version = '0.10', 34 | maintainer = 'Rohit Yadav', 35 | maintainer_email = 'rohit@apache.org', 36 | url = 'https://github.com/shapeblue/ipmisim', 37 | description = "ipmisim is a fake ipmi server", 38 | long_description = "ipmisim is a fake ipmi server", 39 | platforms = ("Any",), 40 | license = 'ASL 2.0', 41 | packages = find_packages(), 42 | install_requires = requires, 43 | include_package_data = True, 44 | zip_safe = False, 45 | classifiers = [ 46 | "Development Status :: 4 - Beta", 47 | "Environment :: Console", 48 | "Intended Audience :: Developers", 49 | "Intended Audience :: End Users/Desktop", 50 | "Operating System :: POSIX :: Linux", 51 | "Programming Language :: Python", 52 | "Topic :: Software Development :: Testing", 53 | "Topic :: Utilities", 54 | ], 55 | entry_points=""" 56 | [console_scripts] 57 | ipmisim = ipmisim.ipmisim:main 58 | """, 59 | ) 60 | -------------------------------------------------------------------------------- /ipmisim/fakebmc.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | # ipmisim - Fake IPMI simulator for testing, forked from Conpot 14 | # Maintainer - Rohit Yadav 15 | # Original Author: Peter Sooky 16 | # Brno University of Technology, Faculty of Information Technology 17 | 18 | import logging 19 | 20 | from pyghmi.ipmi.bmc import Bmc 21 | 22 | 23 | logger = logging.getLogger('ipmisim') 24 | 25 | 26 | class FakeBmc(Bmc): 27 | 28 | def __init__(self, authdata): 29 | self.authdata = authdata 30 | # Initialize fake BMC config 31 | self.deviceid = 0x24 32 | self.revision = 0x10 33 | self.firmwaremajor = 0x10 34 | self.firmwareminor = 0x1 35 | self.ipmiversion = 2 36 | self.additionaldevices = 0 37 | self.mfgid = 0xf 38 | self.prodid = 0xe 39 | 40 | self.powerstate = 'off' 41 | self.bootdevice = 'default' 42 | logger.info('IPMI BMC initialized.') 43 | 44 | def get_boot_device(self): 45 | logger.info('IPMI BMC Get_Boot_Device request.') 46 | return self.bootdevice 47 | 48 | def set_boot_device(self, bootdevice): 49 | logger.info('IPMI BMC Set_Boot_Device request.') 50 | self.bootdevice = bootdevice 51 | 52 | def cold_reset(self): 53 | logger.info('IPMI BMC Cold_Reset request.') 54 | self.powerstate = 'off' 55 | self.bootdevice = 'default' 56 | 57 | def get_power_state(self): 58 | logger.info('IPMI BMC Get_Power_State request.') 59 | return self.powerstate 60 | 61 | def power_off(self): 62 | logger.info('IPMI BMC Power_Off request.') 63 | self.powerstate = 'off' 64 | 65 | def power_on(self): 66 | logger.info('IPMI BMC Power_On request.') 67 | self.powerstate = 'on' 68 | 69 | def power_reset(self): 70 | logger.info('IPMI BMC Power_Reset request.') 71 | # warm boot 72 | self.powerstate = 'on' 73 | 74 | def power_cycle(self): 75 | logger.info('IPMI BMC Power_Cycle request.') 76 | # cold boot 77 | self.powerstate = 'off' 78 | self.powerstate = 'on' 79 | 80 | def power_shutdown(self): 81 | logger.info('IPMI BMC Power_Shutdown request.') 82 | self.powerstate = 'off' 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /ipmisim/fakesession.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | # ipmisim - Fake IPMI simulator for testing, forked from Conpot 14 | # Maintainer - Rohit Yadav 15 | # Original Author: Peter Sooky 16 | # Brno University of Technology, Faculty of Information Technology 17 | 18 | import struct 19 | import os 20 | import socket 21 | import logging 22 | 23 | import pyghmi.exceptions as exc 24 | import pyghmi.ipmi.private.constants as constants 25 | from pyghmi.ipmi.private.session import Session 26 | 27 | import random 28 | import hmac 29 | import hashlib 30 | from Crypto.Cipher import AES 31 | 32 | logger = logging.getLogger('ipmisim') 33 | 34 | def _monotonic_time(): 35 | return os.times()[4] 36 | 37 | class FakeSession(Session): 38 | 39 | def __init__(self, bmc, userid, password, port): 40 | 41 | self.lastpayload = None 42 | self.servermode = True 43 | self.privlevel = 4 44 | self.request_entry = [] 45 | self.socket = None 46 | self.response = None 47 | self.stage = 0 48 | 49 | self.bmc = bmc 50 | self.port = port 51 | self.bmc_handlers = {} 52 | try: 53 | self.userid = userid.encode('utf-8') 54 | self.password = password.encode('utf-8') 55 | except AttributeError: 56 | self.userid = userid 57 | self.password = password 58 | self._initsession() 59 | self.sockaddr = (bmc, port) 60 | self.server = None 61 | self.sol_handler = None 62 | self.ipmicallback = self._generic_callback 63 | logger.info('New IPMI session initialized for client (%s)', self.sockaddr) 64 | 65 | def _generic_callback(self, response): 66 | self.lastresponse = response 67 | 68 | def _ipmi20(self, rawdata): 69 | data = list(struct.unpack("%dB" % len(rawdata), rawdata)) 70 | # payload type numbers in IPMI specification Table 13-16; 6 bits 71 | payload_type = data[5] & 0b00111111 72 | # header = data[:15]; message = data[16:] 73 | if payload_type == 0x10: 74 | # rmcp+ open session request 75 | return self.server._got_rmcp_openrequest(data[16:]) 76 | elif payload_type == 0x11: 77 | # ignore: rmcp+ open session response 78 | return 79 | elif payload_type == 0x12: 80 | # rakp message 1 81 | return self.server._got_rakp1(data[16:]) 82 | elif payload_type == 0x13: 83 | # ignore: rakp message 2 84 | return 85 | elif payload_type == 0x14: 86 | # rakp message 3 87 | return self.server._got_rakp3(data[16:]) 88 | elif payload_type == 0x15: 89 | # ignore: rakp message 4 90 | return 91 | elif payload_type == 0 or payload_type == 1: 92 | # payload_type == 0; IPMI message 93 | # payload_type == 1; SOL(Serial Over Lan) 94 | if not (data[5] & 0b01000000): 95 | # non-authenticated payload 96 | self.server.close_server_session() 97 | return 98 | encryption_bit = 0 99 | if data[5] & 0b10000000: 100 | # using AES-CBC-128 101 | encryption_bit = 1 102 | authcode = rawdata[-12:] 103 | if self.k1 is None: 104 | # we are in no shape to process a packet now 105 | self.server.close_server_session() 106 | return 107 | expectedauthcode = hmac.new(self.k1, rawdata[4:-12], hashlib.sha1).digest()[:12] 108 | if authcode != expectedauthcode: 109 | # BMC failed to assure integrity to us, drop it 110 | self.server.close_server_session() 111 | return 112 | sid = struct.unpack(" 0: 140 | (nextpayload, nextpayloadtype, retry) = self.pendingpayloads.popleft() 141 | self.send_payload(payload=nextpayload, payload_type=nextpayloadtype, retry=retry) 142 | if self.sol_handler: 143 | self.sol_handler(payload) 144 | else: 145 | logger.error('IPMI Unrecognized payload type.') 146 | self.server.close_server_session() 147 | return 148 | 149 | def _ipmi15(self, payload): 150 | self.seqlun = payload[4] 151 | self.clientaddr = payload[3] 152 | self.clientnetfn = (payload[1] >> 2) + 1 153 | self.clientcommand = payload[5] 154 | self._parse_payload(payload) 155 | return 156 | 157 | def _parse_payload(self, payload): 158 | if hasattr(self, 'hasretried'): 159 | if self.hasretried: 160 | self.hasretried = 0 161 | self.tabooseq[(self.expectednetfn, self.expectedcmd, self.seqlun)] = 16 162 | self.expectednetfn = 0x1ff 163 | self.expectedcmd = 0x1ff 164 | self.waiting_sessions.pop(self, None) 165 | self.lastpayload = None 166 | self.last_payload_type = None 167 | response = {} 168 | response['netfn'] = payload[1] >> 2 169 | del payload[0:5] 170 | # remove the trailing checksum 171 | del payload[-1] 172 | response['command'] = payload[0] 173 | del payload[0:1] 174 | response['data'] = payload 175 | self.timeout = 0.5 + (0.5 * random.random()) 176 | self.ipmicallback(response) 177 | 178 | def _send_ipmi_net_payload(self, netfn=None, command=None, data=None, code=0, bridge_request=None, \ 179 | retry=None, delay_xmit=None): 180 | if data is None: 181 | data = [] 182 | if retry is None: 183 | retry = not self.servermode 184 | data = [code] + data 185 | if netfn is None: 186 | netfn = self.clientnetfn 187 | if command is None: 188 | command = self.clientcommand 189 | if data[0] is None and len(data) == 1: 190 | self.server.close_server_session() 191 | return 192 | ipmipayload = self._make_ipmi_payload(netfn, command, bridge_request, data) 193 | payload_type = constants.payload_types['ipmi'] 194 | self.send_payload(payload=ipmipayload, payload_type=payload_type, retry=retry, delay_xmit=delay_xmit) 195 | 196 | def _make_ipmi_payload(self, netfn, command, bridge_request=None, data=()): 197 | bridge_msg = [] 198 | self.expectedcmd = command 199 | self.expectednetfn = netfn + 1 200 | # IPMI spec forbids gaps bigger then 7 in seq number. 201 | seqincrement = 7 202 | 203 | if bridge_request: 204 | addr = bridge_request.get('addr', 0x0) 205 | channel = bridge_request.get('channel', 0x0) 206 | bridge_msg = self._make_bridge_request_msg(channel, netfn, command) 207 | rqaddr = constants.IPMI_BMC_ADDRESS 208 | rsaddr = addr 209 | else: 210 | rqaddr = self.rqaddr 211 | rsaddr = constants.IPMI_BMC_ADDRESS 212 | rsaddr = self.clientaddr 213 | header = [rsaddr, netfn << 2] 214 | 215 | reqbody = [rqaddr, self.seqlun, command] + list(data) 216 | headsum = self.server._checksum(*header) 217 | bodysum = self.server._checksum(*reqbody) 218 | payload = header + [headsum] + reqbody + [bodysum] 219 | if bridge_request: 220 | payload = bridge_msg + payload 221 | tail_csum = self.server._checksum(*payload[3:]) 222 | payload.append(tail_csum) 223 | return payload 224 | 225 | 226 | def _aespad(self, data): 227 | newdata = list(data) 228 | currlen = len(data) + 1 229 | neededpad = currlen % 16 230 | if neededpad: 231 | neededpad = 16 - neededpad 232 | padval = 1 233 | while padval <= neededpad: 234 | newdata.append(padval) 235 | padval += 1 236 | newdata.append(neededpad) 237 | return newdata 238 | 239 | def send_payload(self, payload=(), payload_type=None, retry=True, delay_xmit=None, needskeepalive=False): 240 | if payload and self.lastpayload: 241 | self.pendingpayloads.append((payload, payload_type, retry)) 242 | return 243 | if payload_type is None: 244 | payload_type = self.last_payload_type 245 | if not payload: 246 | payload = self.lastpayload 247 | # constant RMCP header for IPMI 248 | message = [0x6, 0x00, 0xff, 0x07] 249 | if retry: 250 | self.lastpayload = payload 251 | self.last_payload_type = payload_type 252 | message.append(self.authtype) 253 | baretype = payload_type 254 | if self.integrityalgo: 255 | payload_type |= 0b01000000 256 | if self.confalgo: 257 | payload_type |= 0b10000000 258 | 259 | if self.ipmiversion == 2.0: 260 | message.append(payload_type) 261 | if baretype == 2: 262 | raise NotImplementedError("OEM Payloads") 263 | elif baretype not in constants.payload_types.values(): 264 | raise NotImplementedError("Unrecognized payload type %d" % baretype) 265 | message += struct.unpack("!4B", struct.pack("> 8) 287 | iv = os.urandom(16) 288 | message += list(struct.unpack("16B", iv)) 289 | payloadtocrypt = list(map(lambda x: x % 256, self._aespad(payload))) 290 | crypter = AES.new(self.aeskey, AES.MODE_CBC, iv) 291 | crypted = crypter.encrypt(struct.pack("%dB" % len(payloadtocrypt), *payloadtocrypt)) 292 | crypted = list(struct.unpack("%dB" % len(crypted), crypted)) 293 | message += crypted 294 | else: 295 | # no confidetiality algorithm 296 | message.append(psize & 0xff) 297 | message.append(psize >> 8) 298 | message += list(payload) 299 | if self.integrityalgo: 300 | neededpad = (len(message) - 2) % 4 301 | if neededpad: 302 | neededpad = 4 - neededpad 303 | message += [0xff] * neededpad 304 | message.append(neededpad) 305 | message.append(7) 306 | integdata = message[4:] 307 | authcode = hmac.new(self.k1, struct.pack("%dB" % len(integdata), *integdata), 308 | hashlib.sha1).digest()[:12] # SHA1-96 309 | # per RFC2404 truncates to 96 bits 310 | message += struct.unpack("12B", authcode) 311 | self.netpacket = struct.pack("!%dB" % len(message), *message) 312 | self.stage += 1 313 | self._xmit_packet(retry, delay_xmit=delay_xmit) 314 | 315 | def send_ipmi_response(self, data=None, code=0): 316 | if data is None: 317 | data = [] 318 | self._send_ipmi_net_payload(data=data, code=code) 319 | 320 | def _xmit_packet(self, retry=True, delay_xmit=None): 321 | if self.sequencenumber: 322 | self.sequencenumber += 1 323 | if delay_xmit is not None: 324 | # skip transmit, let retry timer do it's thing 325 | self.waiting_sessions[self] = {} 326 | self.waiting_sessions[self]['ipmisession'] = self 327 | self.waiting_sessions[self]['timeout'] = delay_xmit + _monotonic_time() 328 | return 329 | if self.sockaddr: 330 | self.send_data(self.netpacket, self.sockaddr) 331 | else: 332 | self.allsockaddrs = [] 333 | try: 334 | for res in socket.getaddrinfo(self.bmc, self.port, 0, socket.SOCK_DGRAM): 335 | sockaddr = res[4] 336 | if res[0] == socket.AF_INET: 337 | # convert the sockaddr to AF_INET6 338 | newhost = '::ffff:' + sockaddr[0] 339 | sockaddr = (newhost, sockaddr[1], 0, 0) 340 | self.allsockaddrs.append(sockaddr) 341 | self.bmc_handlers[sockaddr] = self 342 | self.send_data(self.netpacket, sockaddr) 343 | except socket.gaierror: 344 | raise exc.IpmiException("Unable to transmit to specified address") 345 | if retry: 346 | self.waiting_sessions[self] = {} 347 | self.waiting_sessions[self]['ipmisession'] = self 348 | self.waiting_sessions[self]['timeout'] = self.timeout + _monotonic_time() 349 | 350 | def send_data(self, packet, address): 351 | logger.debug('IPMI response sent to %s', address) 352 | self.socket.sendto(packet, address) 353 | 354 | 355 | -------------------------------------------------------------------------------- /ipmisim/ipmisim.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); 2 | # you may not use this file except in compliance with the License. 3 | # You may obtain a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, 9 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | # See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | # ipmisim - Fake IPMI simulator for testing, forked from Conpot 14 | # Maintainer - Rohit Yadav 15 | # Original Author: Peter Sooky 16 | # Brno University of Technology, Faculty of Information Technology 17 | 18 | import struct 19 | import os 20 | import sys 21 | 22 | import logging 23 | 24 | import pyghmi.ipmi.private.constants as constants 25 | import pyghmi.ipmi.private.serversession as serversession 26 | 27 | import uuid 28 | import hmac 29 | import hashlib 30 | import collections 31 | 32 | if not __package__ and __name__ == "__main__": 33 | from fakebmc import FakeBmc 34 | from fakesession import FakeSession 35 | else: 36 | from .fakebmc import FakeBmc 37 | from .fakesession import FakeSession 38 | 39 | import socketserver 40 | 41 | from builtins import bytes 42 | 43 | logger = logging.getLogger('ipmisim') 44 | logging.disable(logging.CRITICAL) 45 | 46 | 47 | class IpmiServerContext(object): 48 | 49 | __instance = None 50 | 51 | def __new__(cls, *args, **kwargs): 52 | if cls.__instance == None or (len(args) > 0 and args[0] == 'reset'): 53 | cls.__instance = object.__new__(cls) 54 | cls.__instance.name = "IpmiServer Context" 55 | 56 | # Initialize ctx state 57 | self = cls.__instance 58 | self.device_name = "CloudStack IPMI Sim" 59 | self.sessions = dict() 60 | self.uuid = uuid.uuid4() 61 | self.kg = None 62 | self.authdata = collections.OrderedDict() 63 | 64 | lanchannel = 1 65 | authtype = 0b10000000 66 | authstatus = 0b00000100 67 | chancap = 0b00000010 68 | oemdata = (0, 0, 0, 0) 69 | self.authcap = struct.pack('BBBBBBBBB', 0, lanchannel, authtype, authstatus, chancap, *oemdata) 70 | self.bmc = self._configure_users() 71 | logger.info('CloudStack IPMI Sim BMC initialized') 72 | return cls.__instance 73 | 74 | def _configure_users(self): 75 | # XML parsing 76 | authdata_name = ["admin", "operator", "user"] 77 | authdata_passwd = ["password", "oppassword", "userpassword"] 78 | self.authdata = collections.OrderedDict(zip(authdata_name, authdata_passwd)) 79 | 80 | authdata_priv = [4, 3, 2] 81 | if False in map(lambda k: 0 < int(k) <= 4, authdata_priv): 82 | raise ValueError("Privilege level must be between 1 and 4") 83 | authdata_priv = [int(k) for k in authdata_priv] 84 | self.privdata = collections.OrderedDict(zip(authdata_name, authdata_priv)) 85 | 86 | activeusers = ['true', 'false', 'true'] 87 | self.activeusers = [1, 0, 1] 88 | self.fixedusers = [1, 1, 1] 89 | 90 | self.channelaccessdata = collections.OrderedDict(zip(authdata_name, activeusers)) 91 | 92 | return FakeBmc(self.authdata) 93 | 94 | def _checksum(self, *data): 95 | csum = sum(data) 96 | csum ^= 0xff 97 | csum += 1 98 | csum &= 0xff 99 | return csum 100 | 101 | def handle(self, data, address, socket): 102 | self.sock = socket 103 | # make sure self.session exists 104 | if not (address[0] in self.sessions.keys() and self.sessions[address[0]].port == address[1]) or not hasattr(self, 'session'): 105 | # new session for new source 106 | logger.info('New IPMI traffic from %s', address) 107 | self.session = FakeSession(address[0], "", "", address[1]) 108 | self.session.server = self 109 | 110 | self.uuid = uuid.uuid4() 111 | self.kg = None 112 | 113 | if not hasattr(self, 'session') or not self.session: 114 | return 115 | 116 | self.session.socket = self.sock 117 | self.sessions[address[0]] = self.session 118 | self.initiate_session(data, address, self.session) 119 | else: 120 | # session already exists 121 | logger.debug('Incoming IPMI traffic from %s', address) 122 | if self.session.stage == 0: 123 | self.close_server_session() 124 | else: 125 | self._got_request(data, address, self.session) 126 | 127 | def initiate_session(self, data, address, session): 128 | if len(data) < 22: 129 | self.close_server_session() 130 | return 131 | if not (data[0:1] == b'\x06' and data[2:4] == b'\xff\x07'): 132 | # check rmcp version, sequencenumber and class; 133 | self.close_server_session() 134 | return 135 | if data[4:5] == b'\x06': 136 | # ipmi v2 137 | session.ipmiversion = 2.0 138 | session.authtype = 6 139 | payload_type = data[5:6] 140 | if payload_type not in (b'\x00', b'\x10'): 141 | self.close_server_session() 142 | return 143 | if payload_type == b'\x10': 144 | # new session to handle conversation 145 | serversession.ServerSession(self.authdata, self.kg, session.sockaddr, 146 | self.sock, data[16:], self.uuid, bmc=self) 147 | return 148 | data = data[13:] 149 | myaddr, netfnlun = struct.unpack('2B', data[14:16]) 150 | netfn = (netfnlun & 0b11111100) >> 2 151 | mylun = netfnlun & 0b11 152 | if netfn == 6: 153 | # application request 154 | if data[19:20] == b'\x38': 155 | # cmd = get channel auth capabilities 156 | verchannel, level = struct.unpack('2B', data[20:22]) 157 | version = verchannel & 0b10000000 158 | if version != 0b10000000: 159 | self.close_server_session() 160 | return 161 | channel = verchannel & 0b1111 162 | if channel != 0xe: 163 | self.close_server_session() 164 | return 165 | (clientaddr, clientlun) = struct.unpack('BB', data[17:19]) 166 | level &= 0b1111 167 | self.send_auth_cap(myaddr, mylun, clientaddr, clientlun, session.sockaddr) 168 | 169 | def send_auth_cap(self, myaddr, mylun, clientaddr, clientlun, sockaddr): 170 | header = b'\x06\x00\xff\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10' 171 | 172 | headerdata = (clientaddr, clientlun | (7 << 2)) 173 | headersum = self._checksum(*headerdata) 174 | header += struct.pack('BBBBBB', *(headerdata + (headersum, myaddr, mylun, 0x38))) 175 | header += self.authcap 176 | bodydata = struct.unpack('B' * len(header[17:]), header[17:]) 177 | header += bytes.fromhex(str(self._checksum(*bodydata))) 178 | self.session.stage += 1 179 | logger.debug('Connection established with %s', sockaddr) 180 | self.session.send_data(header, sockaddr) 181 | 182 | def close_server_session(self): 183 | logger.debug('IPMI Session closed %s', self.session.sockaddr[0]) 184 | # cleanup session 185 | del self.sessions[self.session.sockaddr[0]] 186 | del self.session 187 | 188 | def _got_request(self, data, address, session): 189 | if data[4:5] in (b'\x00', b'\x02'): 190 | # ipmi 1.5 payload 191 | session.ipmiversion = 1.5 192 | remsequencenumber = struct.unpack(' 1: 319 | if pendingpriv > self.maxpriv: 320 | returncode = 0x81 321 | else: 322 | self.clientpriv = request['data'][0] 323 | self.session._send_ipmi_net_payload(code=returncode, data=[self.clientpriv]) 324 | logger.debug('IPMI response sent (Set Session Privilege) to %s', self.session.sockaddr) 325 | elif request['netfn'] == 6 and request['command'] == 0x3c: 326 | # close session 327 | self.session.send_ipmi_response() 328 | logger.debug('IPMI response sent (Close Session) to %s', self.session.sockaddr) 329 | self.close_server_session() 330 | elif request['netfn'] == 6 and request['command'] == 0x44: 331 | # get user access 332 | reschan = request['data'][0] 333 | channel = reschan & 0b00001111 334 | resuid = request['data'][1] 335 | usid = resuid & 0b00011111 336 | if self.clientpriv > self.maxpriv: 337 | returncode = 0xd4 338 | else: 339 | returncode = 0 340 | self.usercount = len(authkeys) 341 | self.channelaccess = 0b0000000 | self.privdata[authkeys[usid - 1]] 342 | if self.channelaccessdata[authkeys[usid - 1]] == 'true': 343 | # channelaccess: 7=res; 6=callin; 5=link; 4=messaging; 3-0=privilege 344 | self.channelaccess |= 0b00110000 345 | 346 | data = list() 347 | data.append(self.usercount) 348 | data.append(sum(self.activeusers)) 349 | data.append(sum(self.fixedusers)) 350 | data.append(self.channelaccess) 351 | self.session._send_ipmi_net_payload(code=returncode, data=data) 352 | logger.debug('IPMI response sent (Get User Access) to %s', self.session.sockaddr) 353 | elif request['netfn'] == 6 and request['command'] == 0x46: 354 | # get user name 355 | userid = request['data'][0] 356 | returncode = 0 357 | username = authkeys[userid - 1] 358 | data = list(map(ord, username)) 359 | while len(data) < 16: 360 | # filler 361 | data.append(0) 362 | self.session._send_ipmi_net_payload(code=returncode, data=data) 363 | logger.debug('IPMI response sent (Get User Name) to %s', self.session.sockaddr) 364 | elif request['netfn'] == 6 and request['command'] == 0x45: 365 | # set user name 366 | # TODO: fix issue where users can be overwritten 367 | # python does not support dictionary with duplicate keys 368 | userid = request['data'][0] 369 | username = ''.join(chr(x) for x in request['data'][1:]).strip('\x00') 370 | oldname = authkeys[userid - 1] 371 | # need to recreate dictionary to preserve order 372 | self.copyauth = collections.OrderedDict() 373 | self.copypriv = collections.OrderedDict() 374 | self.copychannel = collections.OrderedDict() 375 | index = 0 376 | for k, v in self.authdata.iteritems(): 377 | if index == userid - 1: 378 | self.copyauth.update({username: self.authdata[oldname]}) 379 | self.copypriv.update({username: self.privdata[oldname]}) 380 | self.copychannel.update({username: self.channelaccessdata[oldname]}) 381 | else: 382 | self.copyauth.update({k: v}) 383 | self.copypriv.update({k: self.privdata[k]}) 384 | self.copychannel.update({k: self.channelaccessdata[k]}) 385 | index += 1 386 | self.authdata = self.copyauth 387 | self.privdata = self.copypriv 388 | self.channelaccessdata = self.copychannel 389 | 390 | returncode = 0 391 | self.session._send_ipmi_net_payload(code=returncode) 392 | logger.debug('IPMI response sent (Set User Name) to %s', self.session.sockaddr) 393 | elif request['netfn'] == 6 and request['command'] == 0x47: 394 | # set user passwd 395 | passwd_length = request['data'][0] & 0b10000000 396 | userid = request['data'][0] & 0b00111111 397 | username = authkeys[userid - 1] 398 | operation = request['data'][1] & 0b00000011 399 | returncode = 0 400 | 401 | if passwd_length: 402 | # 20 byte 403 | passwd = ''.join(chr(x) for x in request['data'][2:22]) 404 | else: 405 | # 16 byte 406 | passwd = ''.join(chr(x) for x in request['data'][2:18]) 407 | if operation == 0: 408 | # disable user 409 | if self.activeusers[self.authdata.keys().index(username)]: 410 | self.activeusers[self.authdata.keys().index(username)] = 0 411 | elif operation == 1: 412 | # enable user 413 | if not self.activeusers[self.authdata.keys().index(username)]: 414 | self.activeusers[self.authdata.keys().index(username)] = 1 415 | elif operation == 2: 416 | # set passwd 417 | if len(passwd) not in [16, 20]: 418 | returncode = 0x81 419 | self.authdata[username] = passwd.strip('\x00') 420 | else: 421 | # test passwd 422 | if len(passwd) not in [16, 20]: 423 | returncode = 0x81 424 | if self.authdata[username] != passwd.strip('\x00'): 425 | returncode = 0x80 426 | 427 | self.session._send_ipmi_net_payload(code=returncode) 428 | logger.info('IPMI response sent (Set User Password) to %s', self.session.sockaddr) 429 | elif request['netfn'] in [0, 6] and request['command'] in [1, 2, 8, 9]: 430 | self.bmc.handle_raw_request(request, self.session) 431 | else: 432 | returncode = 0xc1 433 | self.session._send_ipmi_net_payload(code=returncode) 434 | logger.debug('IPMI unrecognized command from %s', self.session.sockaddr) 435 | logger.debug('IPMI response sent (Invalid Command) to %s', self.session.sockaddr) 436 | 437 | 438 | class IpmiServer(socketserver.BaseRequestHandler): 439 | def handle(self): 440 | data = self.request[0] 441 | socket = self.request[1] 442 | address = self.client_address 443 | return IpmiServerContext().handle(data, address, socket) 444 | 445 | 446 | class ThreadedIpmiServer(socketserver.ThreadingMixIn, socketserver.UDPServer): 447 | pass 448 | 449 | 450 | def main(): 451 | logging.disable(logging.NOTSET) 452 | logger.setLevel(logging.INFO) 453 | 454 | ch = logging.StreamHandler(sys.stdout) 455 | ch.setLevel(logging.DEBUG) 456 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 457 | ch.setFormatter(formatter) 458 | logger.addHandler(ch) 459 | 460 | port = 9001 461 | if len(sys.argv) > 1: 462 | port = int(sys.argv[1]) 463 | 464 | # Initialize context 465 | ctx = IpmiServerContext() 466 | 467 | try: 468 | ThreadedIpmiServer.allow_reuse_address = True 469 | server = ThreadedIpmiServer(('0.0.0.0', port), IpmiServer) 470 | logger.info("Started IPMI Server on 0.0.0.0:" + str(port)) 471 | server.serve_forever() 472 | except KeyboardInterrupt: 473 | server.shutdown() 474 | server.server_close() 475 | sys.exit(0) 476 | 477 | 478 | if __name__ == "__main__": 479 | main() 480 | --------------------------------------------------------------------------------