@@ -101,11 +176,24 @@ Form.propTypes = {
submitting: PropTypes.bool,
invalid: PropTypes.bool,
interfaces: PropTypes.object,
+ interfaceValue: PropTypes.string,
initialValues: PropTypes.object,
processingConfig: PropTypes.bool,
+ processingInterfaces: PropTypes.bool,
+ enabled: PropTypes.bool,
t: PropTypes.func,
};
+
+const selector = formValueSelector('dhcpForm');
+
+Form = connect((state) => {
+ const interfaceValue = selector(state, 'interface_name');
+ return {
+ interfaceValue,
+ };
+})(Form);
+
export default flow([
withNamespaces(),
reduxForm({ form: 'dhcpForm' }),
diff --git a/client/src/components/Settings/Dhcp/Interface.js b/client/src/components/Settings/Dhcp/Interface.js
deleted file mode 100644
index 3b9d3e03..00000000
--- a/client/src/components/Settings/Dhcp/Interface.js
+++ /dev/null
@@ -1,114 +0,0 @@
-import React from 'react';
-import { connect } from 'react-redux';
-import PropTypes from 'prop-types';
-import { Field, reduxForm, formValueSelector } from 'redux-form';
-import { withNamespaces, Trans } from 'react-i18next';
-import flow from 'lodash/flow';
-
-const renderInterfaces = (interfaces => (
- Object.keys(interfaces).map((item) => {
- const option = interfaces[item];
- const { name } = option;
- const onlyIPv6 = option.ip_addresses.every(ip => ip.includes(':'));
- let interfaceIP = option.ip_addresses[0];
-
- if (!onlyIPv6) {
- option.ip_addresses.forEach((ip) => {
- if (!ip.includes(':')) {
- interfaceIP = ip;
- }
- });
- }
-
- return (
-
- );
- })
-));
-
-const renderInterfaceValues = (interfaceValues => (
-
- -
- MTU:
- {interfaceValues.mtu}
-
- -
- dhcp_hardware_address:
- {interfaceValues.hardware_address}
-
- -
- dhcp_ip_addresses:
- {
- interfaceValues.ip_addresses
- .map(ip => {ip})
- }
-
-
-));
-
-let Interface = (props) => {
- const {
- t,
- handleChange,
- interfaces,
- processing,
- interfaceValue,
- enabled,
- } = props;
-
- return (
-
- );
-};
-
-Interface.propTypes = {
- handleChange: PropTypes.func,
- interfaces: PropTypes.object,
- processing: PropTypes.bool,
- interfaceValue: PropTypes.string,
- initialValues: PropTypes.object,
- enabled: PropTypes.bool,
- t: PropTypes.func,
-};
-
-const selector = formValueSelector('dhcpInterface');
-
-Interface = connect((state) => {
- const interfaceValue = selector(state, 'interface_name');
- return {
- interfaceValue,
- };
-})(Interface);
-
-export default flow([
- withNamespaces(),
- reduxForm({ form: 'dhcpInterface' }),
-])(Interface);
diff --git a/client/src/components/Settings/Dhcp/StaticLeases/Form.js b/client/src/components/Settings/Dhcp/StaticLeases/Form.js
new file mode 100644
index 00000000..6695a6b3
--- /dev/null
+++ b/client/src/components/Settings/Dhcp/StaticLeases/Form.js
@@ -0,0 +1,96 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Field, reduxForm } from 'redux-form';
+import { Trans, withNamespaces } from 'react-i18next';
+import flow from 'lodash/flow';
+
+import { renderField, ipv4, mac, required } from '../../../../helpers/form';
+
+const Form = (props) => {
+ const {
+ t,
+ handleSubmit,
+ reset,
+ pristine,
+ submitting,
+ toggleLeaseModal,
+ processingAdding,
+ } = props;
+
+ return (
+
+ );
+};
+
+Form.propTypes = {
+ pristine: PropTypes.bool.isRequired,
+ handleSubmit: PropTypes.func.isRequired,
+ reset: PropTypes.func.isRequired,
+ submitting: PropTypes.bool.isRequired,
+ toggleLeaseModal: PropTypes.func.isRequired,
+ processingAdding: PropTypes.bool.isRequired,
+ t: PropTypes.func.isRequired,
+};
+
+export default flow([
+ withNamespaces(),
+ reduxForm({ form: 'leaseForm' }),
+])(Form);
diff --git a/client/src/components/Settings/Dhcp/StaticLeases/Modal.js b/client/src/components/Settings/Dhcp/StaticLeases/Modal.js
new file mode 100644
index 00000000..6291f274
--- /dev/null
+++ b/client/src/components/Settings/Dhcp/StaticLeases/Modal.js
@@ -0,0 +1,49 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Trans, withNamespaces } from 'react-i18next';
+import ReactModal from 'react-modal';
+
+import Form from './Form';
+
+const Modal = (props) => {
+ const {
+ isModalOpen,
+ handleSubmit,
+ toggleLeaseModal,
+ processingAdding,
+ } = props;
+
+ return (
+
toggleLeaseModal()}
+ >
+
+
+
+ dhcp_new_static_lease
+
+
+
+
+
+
+ );
+};
+
+Modal.propTypes = {
+ isModalOpen: PropTypes.bool.isRequired,
+ handleSubmit: PropTypes.func.isRequired,
+ toggleLeaseModal: PropTypes.func.isRequired,
+ processingAdding: PropTypes.bool.isRequired,
+};
+
+export default withNamespaces()(Modal);
diff --git a/client/src/components/Settings/Dhcp/StaticLeases/index.js b/client/src/components/Settings/Dhcp/StaticLeases/index.js
new file mode 100644
index 00000000..e96e806e
--- /dev/null
+++ b/client/src/components/Settings/Dhcp/StaticLeases/index.js
@@ -0,0 +1,112 @@
+import React, { Component, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import ReactTable from 'react-table';
+import { Trans, withNamespaces } from 'react-i18next';
+
+import Modal from './Modal';
+
+class StaticLeases extends Component {
+ cellWrap = ({ value }) => (
+
+
+ {value}
+
+
+ );
+
+ handleSubmit = (data) => {
+ this.props.addStaticLease(data);
+ }
+
+ handleDelete = (ip, mac, hostname = '') => {
+ const name = hostname || ip;
+ // eslint-disable-next-line no-alert
+ if (window.confirm(this.props.t('delete_confirm', { key: name }))) {
+ this.props.removeStaticLease({ ip, mac, hostname });
+ }
+ }
+
+ render() {
+ const {
+ isModalOpen,
+ toggleLeaseModal,
+ processingAdding,
+ processingDeleting,
+ staticLeases,
+ t,
+ } = this.props;
+ return (
+
+ dhcp_table_hostname,
+ accessor: 'hostname',
+ Cell: this.cellWrap,
+ },
+ {
+ Header: actions_table_header,
+ accessor: 'actions',
+ maxWidth: 150,
+ Cell: (row) => {
+ const { ip, mac, hostname } = row.original;
+
+ return (
+
+
+
+ );
+ },
+ },
+ ]}
+ showPagination={false}
+ noDataText={t('dhcp_static_leases_not_found')}
+ className="-striped -highlight card-table-overflow"
+ minRows={6}
+ />
+
+
+ );
+ }
+}
+
+StaticLeases.propTypes = {
+ staticLeases: PropTypes.array.isRequired,
+ isModalOpen: PropTypes.bool.isRequired,
+ toggleLeaseModal: PropTypes.func.isRequired,
+ removeStaticLease: PropTypes.func.isRequired,
+ addStaticLease: PropTypes.func.isRequired,
+ processingAdding: PropTypes.bool.isRequired,
+ processingDeleting: PropTypes.bool.isRequired,
+ t: PropTypes.func.isRequired,
+};
+
+export default withNamespaces()(StaticLeases);
diff --git a/client/src/components/Settings/Dhcp/index.js b/client/src/components/Settings/Dhcp/index.js
index 437f9265..d33f1bcf 100644
--- a/client/src/components/Settings/Dhcp/index.js
+++ b/client/src/components/Settings/Dhcp/index.js
@@ -6,13 +6,15 @@ import { Trans, withNamespaces } from 'react-i18next';
import { DHCP_STATUS_RESPONSE } from '../../../helpers/constants';
import Form from './Form';
import Leases from './Leases';
-import Interface from './Interface';
+import StaticLeases from './StaticLeases/index';
import Card from '../../ui/Card';
import Accordion from '../../ui/Accordion';
class Dhcp extends Component {
handleFormSubmit = (values) => {
- this.props.setDhcpConfig(values);
+ if (values.interface_name) {
+ this.props.setDhcpConfig(values);
+ }
};
handleToggle = (config) => {
@@ -168,18 +170,16 @@ class Dhcp extends Component {
{!dhcp.processing &&
-
@@ -188,11 +188,11 @@ class Dhcp extends Component {
type="button"
className={statusButtonClass}
onClick={() =>
- this.props.findActiveDhcp(dhcp.config.interface_name)
+ this.props.findActiveDhcp(interface_name)
}
disabled={
- dhcp.config.enabled
- || !dhcp.config.interface_name
+ enabled
+ || !interface_name
|| dhcp.processingConfig
}
>
@@ -211,13 +211,39 @@ class Dhcp extends Component {
{!dhcp.processing && dhcp.config.enabled &&
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
}
);
@@ -231,6 +257,9 @@ Dhcp.propTypes = {
setDhcpConfig: PropTypes.func,
findActiveDhcp: PropTypes.func,
handleSubmit: PropTypes.func,
+ addStaticLease: PropTypes.func,
+ removeStaticLease: PropTypes.func,
+ toggleLeaseModal: PropTypes.func,
t: PropTypes.func,
};
diff --git a/client/src/components/Settings/index.js b/client/src/components/Settings/index.js
index 3435e610..8d36c6c4 100644
--- a/client/src/components/Settings/index.js
+++ b/client/src/components/Settings/index.js
@@ -128,6 +128,9 @@ class Settings extends Component {
getDhcpStatus={this.props.getDhcpStatus}
findActiveDhcp={this.props.findActiveDhcp}
setDhcpConfig={this.props.setDhcpConfig}
+ addStaticLease={this.props.addStaticLease}
+ removeStaticLease={this.props.removeStaticLease}
+ toggleLeaseModal={this.props.toggleLeaseModal}
/>
diff --git a/client/src/containers/Settings.js b/client/src/containers/Settings.js
index 95be768b..0255e044 100644
--- a/client/src/containers/Settings.js
+++ b/client/src/containers/Settings.js
@@ -11,6 +11,9 @@ import {
getDhcpInterfaces,
setDhcpConfig,
findActiveDhcp,
+ addStaticLease,
+ removeStaticLease,
+ toggleLeaseModal,
} from '../actions';
import {
getTlsStatus,
@@ -62,6 +65,9 @@ const mapDispatchToProps = {
updateClient,
deleteClient,
toggleClientModal,
+ addStaticLease,
+ removeStaticLease,
+ toggleLeaseModal,
};
export default connect(
diff --git a/client/src/reducers/index.js b/client/src/reducers/index.js
index 0a3b8171..e9a012f8 100644
--- a/client/src/reducers/index.js
+++ b/client/src/reducers/index.js
@@ -287,11 +287,18 @@ const dhcp = handleActions({
[actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }),
[actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }),
[actions.getDhcpStatusSuccess]: (state, { payload }) => {
+ const {
+ static_leases: staticLeases,
+ ...values
+ } = payload;
+
const newState = {
...state,
- ...payload,
+ staticLeases,
processing: false,
+ ...values,
};
+
return newState;
},
@@ -344,17 +351,62 @@ const dhcp = handleActions({
const newState = { ...state, config: newConfig, processingConfig: false };
return newState;
},
+
+ [actions.toggleLeaseModal]: (state) => {
+ const newState = {
+ ...state,
+ isModalOpen: !state.isModalOpen,
+ };
+ return newState;
+ },
+
+ [actions.addStaticLeaseRequest]: state => ({ ...state, processingAdding: true }),
+ [actions.addStaticLeaseFailure]: state => ({ ...state, processingAdding: false }),
+ [actions.addStaticLeaseSuccess]: (state, { payload }) => {
+ const {
+ ip, mac, hostname,
+ } = payload;
+ const newLease = {
+ ip,
+ mac,
+ hostname: hostname || '',
+ };
+ const leases = [...state.staticLeases, newLease];
+ const newState = {
+ ...state,
+ staticLeases: leases,
+ processingAdding: false,
+ };
+ return newState;
+ },
+
+ [actions.removeStaticLeaseRequest]: state => ({ ...state, processingDeleting: true }),
+ [actions.removeStaticLeaseFailure]: state => ({ ...state, processingDeleting: false }),
+ [actions.removeStaticLeaseSuccess]: (state, { payload }) => {
+ const leaseToRemove = payload.ip;
+ const leases = state.staticLeases.filter(item => item.ip !== leaseToRemove);
+ const newState = {
+ ...state,
+ staticLeases: leases,
+ processingDeleting: false,
+ };
+ return newState;
+ },
}, {
processing: true,
processingStatus: false,
processingInterfaces: false,
processingDhcp: false,
processingConfig: false,
+ processingAdding: false,
+ processingDeleting: false,
config: {
enabled: false,
},
check: null,
leases: [],
+ staticLeases: [],
+ isModalOpen: false,
});
export default combineReducers({
diff --git a/control.go b/control.go
index 2be9b8ba..77970de2 100644
--- a/control.go
+++ b/control.go
@@ -993,6 +993,8 @@ func registerControlHandlers() {
http.HandleFunc("/control/dhcp/interfaces", postInstall(optionalAuth(ensureGET(handleDHCPInterfaces))))
http.HandleFunc("/control/dhcp/set_config", postInstall(optionalAuth(ensurePOST(handleDHCPSetConfig))))
http.HandleFunc("/control/dhcp/find_active_dhcp", postInstall(optionalAuth(ensurePOST(handleDHCPFindActiveServer))))
+ http.HandleFunc("/control/dhcp/add_static_lease", postInstall(optionalAuth(ensurePOST(handleDHCPAddStaticLease))))
+ http.HandleFunc("/control/dhcp/remove_static_lease", postInstall(optionalAuth(ensurePOST(handleDHCPRemoveStaticLease))))
RegisterTLSHandlers()
RegisterClientsHandlers()
diff --git a/dhcp.go b/dhcp.go
index 4bd0c463..8c966ad6 100644
--- a/dhcp.go
+++ b/dhcp.go
@@ -20,23 +20,33 @@ import (
var dhcpServer = dhcpd.Server{}
+// []dhcpd.Lease -> JSON
+func convertLeases(inputLeases []dhcpd.Lease, includeExpires bool) []map[string]string {
+ leases := []map[string]string{}
+ for _, l := range inputLeases {
+ lease := map[string]string{
+ "mac": l.HWAddr.String(),
+ "ip": l.IP.String(),
+ "hostname": l.Hostname,
+ }
+
+ if includeExpires {
+ lease["expires"] = l.Expiry.Format(time.RFC3339)
+ }
+
+ leases = append(leases, lease)
+ }
+ return leases
+}
+
func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
- rawLeases := dhcpServer.Leases()
- leases := []map[string]string{}
- for i := range rawLeases {
- lease := map[string]string{
- "mac": rawLeases[i].HWAddr.String(),
- "ip": rawLeases[i].IP.String(),
- "hostname": rawLeases[i].Hostname,
- "expires": rawLeases[i].Expiry.Format(time.RFC3339),
- }
- leases = append(leases, lease)
-
- }
+ leases := convertLeases(dhcpServer.Leases(), true)
+ staticLeases := convertLeases(dhcpServer.StaticLeases(), false)
status := map[string]interface{}{
- "config": config.DHCP,
- "leases": leases,
+ "config": config.DHCP,
+ "leases": leases,
+ "static_leases": staticLeases,
}
w.Header().Set("Content-Type", "application/json")
@@ -47,20 +57,43 @@ func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
}
}
+type leaseJSON struct {
+ HWAddr string `json:"mac"`
+ IP string `json:"ip"`
+ Hostname string `json:"hostname"`
+}
+
+type dhcpServerConfigJSON struct {
+ dhcpd.ServerConfig `json:",inline"`
+ StaticLeases []leaseJSON `json:"static_leases"`
+}
+
func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
log.Tracef("%s %v", r.Method, r.URL)
- newconfig := dhcpd.ServerConfig{}
+ newconfig := dhcpServerConfigJSON{}
err := json.NewDecoder(r.Body).Decode(&newconfig)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err)
return
}
+ err = dhcpServer.CheckConfig(newconfig.ServerConfig)
+ if err != nil {
+ httpError(w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err)
+ return
+ }
+
err = dhcpServer.Stop()
if err != nil {
log.Error("failed to stop the DHCP server: %s", err)
}
+ err = dhcpServer.Init(newconfig.ServerConfig)
+ if err != nil {
+ httpError(w, http.StatusBadRequest, "Invalid DHCP configuration: %s", err)
+ return
+ }
+
if newconfig.Enabled {
staticIP, err := hasStaticIP(newconfig.InterfaceName)
@@ -72,14 +105,14 @@ func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
}
}
- err = dhcpServer.Start(&newconfig)
+ err = dhcpServer.Start()
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to start DHCP server: %s", err)
return
}
}
- config.DHCP = newconfig
+ config.DHCP = newconfig.ServerConfig
httpUpdateConfigReloadDNSReturnOK(w, r)
}
@@ -333,12 +366,80 @@ func setStaticIP(ifaceName string) error {
return nil
}
+func handleDHCPAddStaticLease(w http.ResponseWriter, r *http.Request) {
+ log.Tracef("%s %v", r.Method, r.URL)
+
+ lj := leaseJSON{}
+ err := json.NewDecoder(r.Body).Decode(&lj)
+ if err != nil {
+ httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
+ return
+ }
+
+ ip := parseIPv4(lj.IP)
+ if ip == nil {
+ httpError(w, http.StatusBadRequest, "invalid IP")
+ return
+ }
+
+ mac, _ := net.ParseMAC(lj.HWAddr)
+
+ lease := dhcpd.Lease{
+ IP: ip,
+ HWAddr: mac,
+ Hostname: lj.Hostname,
+ }
+ err = dhcpServer.AddStaticLease(lease)
+ if err != nil {
+ httpError(w, http.StatusBadRequest, "%s", err)
+ return
+ }
+ returnOK(w)
+}
+
+func handleDHCPRemoveStaticLease(w http.ResponseWriter, r *http.Request) {
+ log.Tracef("%s %v", r.Method, r.URL)
+
+ lj := leaseJSON{}
+ err := json.NewDecoder(r.Body).Decode(&lj)
+ if err != nil {
+ httpError(w, http.StatusBadRequest, "json.Decode: %s", err)
+ return
+ }
+
+ ip := parseIPv4(lj.IP)
+ if ip == nil {
+ httpError(w, http.StatusBadRequest, "invalid IP")
+ return
+ }
+
+ mac, _ := net.ParseMAC(lj.HWAddr)
+
+ lease := dhcpd.Lease{
+ IP: ip,
+ HWAddr: mac,
+ Hostname: lj.Hostname,
+ }
+ err = dhcpServer.RemoveStaticLease(lease)
+ if err != nil {
+ httpError(w, http.StatusBadRequest, "%s", err)
+ return
+ }
+ returnOK(w)
+}
+
func startDHCPServer() error {
if !config.DHCP.Enabled {
// not enabled, don't do anything
return nil
}
- err := dhcpServer.Start(&config.DHCP)
+
+ err := dhcpServer.Init(config.DHCP)
+ if err != nil {
+ return errorx.Decorate(err, "Couldn't init DHCP server")
+ }
+
+ err = dhcpServer.Start()
if err != nil {
return errorx.Decorate(err, "Couldn't start DHCP server")
}
@@ -350,10 +451,6 @@ func stopDHCPServer() error {
return nil
}
- if !dhcpServer.Enabled {
- return nil
- }
-
err := dhcpServer.Stop()
if err != nil {
return errorx.Decorate(err, "Couldn't stop DHCP server")
diff --git a/dhcpd/db.go b/dhcpd/db.go
index 1caa7d81..c27bb679 100644
--- a/dhcpd/db.go
+++ b/dhcpd/db.go
@@ -23,8 +23,20 @@ type leaseJSON struct {
Expiry int64 `json:"exp"`
}
+// Safe version of dhcp4.IPInRange()
+func ipInRange(start, stop, ip net.IP) bool {
+ if len(start) != len(stop) ||
+ len(start) != len(ip) {
+ return false
+ }
+ return dhcp4.IPInRange(start, stop, ip)
+}
+
// Load lease table from DB
func (s *Server) dbLoad() {
+ s.leases = nil
+ s.IPpool = make(map[[4]byte]net.HardwareAddr)
+
data, err := ioutil.ReadFile(dbFilename)
if err != nil {
if !os.IsNotExist(err) {
@@ -40,13 +52,12 @@ func (s *Server) dbLoad() {
return
}
- s.leases = nil
- s.IPpool = make(map[[4]byte]net.HardwareAddr)
-
numLeases := len(obj)
for i := range obj {
- if !dhcp4.IPInRange(s.leaseStart, s.leaseStop, obj[i].IP) {
+ if obj[i].Expiry != leaseExpireStatic &&
+ !ipInRange(s.leaseStart, s.leaseStop, obj[i].IP) {
+
log.Tracef("Skipping a lease with IP %s: not within current IP range", obj[i].IP)
continue
}
diff --git a/dhcpd/dhcpd.go b/dhcpd/dhcpd.go
index fd3e807a..13089f7d 100644
--- a/dhcpd/dhcpd.go
+++ b/dhcpd/dhcpd.go
@@ -14,6 +14,7 @@ import (
)
const defaultDiscoverTime = time.Second * 3
+const leaseExpireStatic = 1
// Lease contains the necessary information about a DHCP lease
// field ordering is important -- yaml fields will mirror ordering from here
@@ -21,7 +22,10 @@ type Lease struct {
HWAddr net.HardwareAddr `json:"mac" yaml:"hwaddr"`
IP net.IP `json:"ip"`
Hostname string `json:"hostname"`
- Expiry time.Time `json:"expires"`
+
+ // Lease expiration time
+ // 1: static lease
+ Expiry time.Time `json:"expires"`
}
// ServerConfig - DHCP server configuration
@@ -53,6 +57,7 @@ type Server struct {
// leases
leases []*Lease
+ leasesLock sync.RWMutex
leaseStart net.IP // parsed from config RangeStart
leaseStop net.IP // parsed from config RangeEnd
leaseTime time.Duration // parsed from config LeaseDuration
@@ -61,8 +66,7 @@ type Server struct {
// IP address pool -- if entry is in the pool, then it's attached to a lease
IPpool map[[4]byte]net.HardwareAddr
- ServerConfig
- sync.RWMutex
+ conf ServerConfig
}
// Print information about the available network interfaces
@@ -75,62 +79,65 @@ func printInterfaces() {
log.Info("Available network interfaces: %s", buf.String())
}
-// Start will listen on port 67 and serve DHCP requests.
-// Even though config can be nil, it is not optional (at least for now), since there are no default values (yet).
-func (s *Server) Start(config *ServerConfig) error {
- if config != nil {
- s.ServerConfig = *config
- }
+// CheckConfig checks the configuration
+func (s *Server) CheckConfig(config ServerConfig) error {
+ tmpServer := Server{}
+ return tmpServer.setConfig(config)
+}
- iface, err := net.InterfaceByName(s.InterfaceName)
+// Init checks the configuration and initializes the server
+func (s *Server) Init(config ServerConfig) error {
+ err := s.setConfig(config)
+ if err != nil {
+ return err
+ }
+ s.dbLoad()
+ return nil
+}
+
+func (s *Server) setConfig(config ServerConfig) error {
+ s.conf = config
+
+ iface, err := net.InterfaceByName(config.InterfaceName)
if err != nil {
- s.closeConn() // in case it was already started
printInterfaces()
- return wrapErrPrint(err, "Couldn't find interface by name %s", s.InterfaceName)
+ return wrapErrPrint(err, "Couldn't find interface by name %s", config.InterfaceName)
}
// get ipv4 address of an interface
s.ipnet = getIfaceIPv4(iface)
if s.ipnet == nil {
- s.closeConn() // in case it was already started
- return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", s.InterfaceName, iface)
+ return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", config.InterfaceName, iface)
}
- if s.LeaseDuration == 0 {
+ if config.LeaseDuration == 0 {
s.leaseTime = time.Hour * 2
- s.LeaseDuration = uint(s.leaseTime.Seconds())
} else {
- s.leaseTime = time.Second * time.Duration(s.LeaseDuration)
+ s.leaseTime = time.Second * time.Duration(config.LeaseDuration)
}
- s.leaseStart, err = parseIPv4(s.RangeStart)
+ s.leaseStart, err = parseIPv4(config.RangeStart)
if err != nil {
-
- s.closeConn() // in case it was already started
- return wrapErrPrint(err, "Failed to parse range start address %s", s.RangeStart)
+ return wrapErrPrint(err, "Failed to parse range start address %s", config.RangeStart)
}
- s.leaseStop, err = parseIPv4(s.RangeEnd)
+ s.leaseStop, err = parseIPv4(config.RangeEnd)
if err != nil {
- s.closeConn() // in case it was already started
- return wrapErrPrint(err, "Failed to parse range end address %s", s.RangeEnd)
+ return wrapErrPrint(err, "Failed to parse range end address %s", config.RangeEnd)
}
- subnet, err := parseIPv4(s.SubnetMask)
+ subnet, err := parseIPv4(config.SubnetMask)
if err != nil {
- s.closeConn() // in case it was already started
- return wrapErrPrint(err, "Failed to parse subnet mask %s", s.SubnetMask)
+ return wrapErrPrint(err, "Failed to parse subnet mask %s", config.SubnetMask)
}
// if !bytes.Equal(subnet, s.ipnet.Mask) {
- // s.closeConn() // in case it was already started
// return wrapErrPrint(err, "specified subnet mask %s does not meatch interface %s subnet mask %s", s.SubnetMask, s.InterfaceName, s.ipnet.Mask)
// }
- router, err := parseIPv4(s.GatewayIP)
+ router, err := parseIPv4(config.GatewayIP)
if err != nil {
- s.closeConn() // in case it was already started
- return wrapErrPrint(err, "Failed to parse gateway IP %s", s.GatewayIP)
+ return wrapErrPrint(err, "Failed to parse gateway IP %s", config.GatewayIP)
}
s.leaseOptions = dhcp4.Options{
@@ -139,12 +146,21 @@ func (s *Server) Start(config *ServerConfig) error {
dhcp4.OptionDomainNameServer: s.ipnet.IP,
}
+ return nil
+}
+
+// Start will listen on port 67 and serve DHCP requests.
+func (s *Server) Start() error {
+
// TODO: don't close if interface and addresses are the same
if s.conn != nil {
s.closeConn()
}
- s.dbLoad()
+ iface, err := net.InterfaceByName(s.conf.InterfaceName)
+ if err != nil {
+ return wrapErrPrint(err, "Couldn't find interface by name %s", s.conf.InterfaceName)
+ }
c, err := newFilterConn(*iface, ":67") // it has to be bound to 0.0.0.0:67, otherwise it won't see DHCP discover/request packets
if err != nil {
@@ -229,9 +245,9 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
log.Tracef("Assigning IP address %s to %s (lease for %s expired at %s)",
s.leases[i].IP, hwaddr, s.leases[i].HWAddr, s.leases[i].Expiry)
lease.IP = s.leases[i].IP
- s.Lock()
+ s.leasesLock.Lock()
s.leases[i] = lease
- s.Unlock()
+ s.leasesLock.Unlock()
s.reserveIP(lease.IP, hwaddr)
return lease, nil
@@ -239,9 +255,9 @@ func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
log.Tracef("Assigning to %s IP address %s", hwaddr, ip.String())
lease.IP = ip
- s.Lock()
+ s.leasesLock.Lock()
s.leases = append(s.leases, lease)
- s.Unlock()
+ s.leasesLock.Unlock()
return lease, nil
}
@@ -261,7 +277,7 @@ func (s *Server) findLease(p dhcp4.Packet) *Lease {
func (s *Server) findExpiredLease() int {
now := time.Now().Unix()
for i, lease := range s.leases {
- if lease.Expiry.Unix() <= now {
+ if lease.Expiry.Unix() <= now && lease.Expiry.Unix() != leaseExpireStatic {
return i
}
}
@@ -269,11 +285,6 @@ func (s *Server) findExpiredLease() int {
}
func (s *Server) findFreeIP(hwaddr net.HardwareAddr) (net.IP, error) {
- // if IP pool is nil, lazy initialize it
- if s.IPpool == nil {
- s.IPpool = make(map[[4]byte]net.HardwareAddr)
- }
-
// go from start to end, find unreserved IP
var foundIP net.IP
for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); i++ {
@@ -361,7 +372,7 @@ func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dh
// Return TRUE if it doesn't reply, which probably means that the IP is available
func (s *Server) addrAvailable(target net.IP) bool {
- if s.ICMPTimeout == 0 {
+ if s.conf.ICMPTimeout == 0 {
return true
}
@@ -372,7 +383,7 @@ func (s *Server) addrAvailable(target net.IP) bool {
}
pinger.SetPrivileged(true)
- pinger.Timeout = time.Duration(s.ICMPTimeout) * time.Millisecond
+ pinger.Timeout = time.Duration(s.conf.ICMPTimeout) * time.Millisecond
pinger.Count = 1
reply := false
pinger.OnRecv = func(pkt *ping.Packet) {
@@ -395,11 +406,11 @@ func (s *Server) addrAvailable(target net.IP) bool {
func (s *Server) blacklistLease(lease *Lease) {
hw := make(net.HardwareAddr, 6)
s.reserveIP(lease.IP, hw)
- s.Lock()
+ s.leasesLock.Lock()
lease.HWAddr = hw
lease.Hostname = ""
lease.Expiry = time.Now().Add(s.leaseTime)
- s.Unlock()
+ s.leasesLock.Unlock()
}
// Return TRUE if DHCP packet is correct
@@ -516,21 +527,103 @@ func (s *Server) handleDecline(p dhcp4.Packet, options dhcp4.Options) dhcp4.Pack
return nil
}
+// AddStaticLease adds a static lease (thread-safe)
+func (s *Server) AddStaticLease(l Lease) error {
+ if s.IPpool == nil {
+ return fmt.Errorf("DHCP server isn't started")
+ }
+
+ if len(l.IP) != 4 {
+ return fmt.Errorf("Invalid IP")
+ }
+ if len(l.HWAddr) != 6 {
+ return fmt.Errorf("Invalid MAC")
+ }
+ l.Expiry = time.Unix(leaseExpireStatic, 0)
+
+ s.leasesLock.Lock()
+ defer s.leasesLock.Unlock()
+
+ if s.findReservedHWaddr(l.IP) != nil {
+ return fmt.Errorf("IP is already used")
+ }
+ s.leases = append(s.leases, &l)
+ s.reserveIP(l.IP, l.HWAddr)
+ s.dbStore()
+ return nil
+}
+
+// RemoveStaticLease removes a static lease (thread-safe)
+func (s *Server) RemoveStaticLease(l Lease) error {
+ if s.IPpool == nil {
+ return fmt.Errorf("DHCP server isn't started")
+ }
+
+ if len(l.IP) != 4 {
+ return fmt.Errorf("Invalid IP")
+ }
+ if len(l.HWAddr) != 6 {
+ return fmt.Errorf("Invalid MAC")
+ }
+
+ s.leasesLock.Lock()
+ defer s.leasesLock.Unlock()
+
+ if s.findReservedHWaddr(l.IP) == nil {
+ return fmt.Errorf("Lease not found")
+ }
+
+ var newLeases []*Lease
+ for _, lease := range s.leases {
+ if bytes.Equal(lease.IP.To4(), l.IP) {
+ if !bytes.Equal(lease.HWAddr, l.HWAddr) ||
+ lease.Hostname != l.Hostname {
+ return fmt.Errorf("Lease not found")
+ }
+ continue
+ }
+ newLeases = append(newLeases, lease)
+ }
+ s.leases = newLeases
+ s.unreserveIP(l.IP)
+ s.dbStore()
+ return nil
+}
+
// Leases returns the list of current DHCP leases (thread-safe)
func (s *Server) Leases() []Lease {
var result []Lease
now := time.Now().Unix()
- s.RLock()
+ s.leasesLock.RLock()
for _, lease := range s.leases {
if lease.Expiry.Unix() > now {
result = append(result, *lease)
}
}
- s.RUnlock()
+ s.leasesLock.RUnlock()
return result
}
+// StaticLeases returns the list of statically-configured DHCP leases (thread-safe)
+func (s *Server) StaticLeases() []Lease {
+ s.leasesLock.Lock()
+ if s.IPpool == nil {
+ s.dbLoad()
+ }
+ s.leasesLock.Unlock()
+
+ var result []Lease
+ s.leasesLock.RLock()
+ for _, lease := range s.leases {
+ if lease.Expiry.Unix() == 1 {
+ result = append(result, *lease)
+ }
+ }
+ s.leasesLock.RUnlock()
+ return result
+}
+
// Print information about the current leases
func (s *Server) printLeases() {
log.Tracef("Leases:")
@@ -543,8 +636,8 @@ func (s *Server) printLeases() {
// FindIPbyMAC finds an IP address by MAC address in the currently active DHCP leases
func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP {
now := time.Now().Unix()
- s.RLock()
- defer s.RUnlock()
+ s.leasesLock.RLock()
+ defer s.leasesLock.RUnlock()
for _, l := range s.leases {
if l.Expiry.Unix() > now && bytes.Equal(mac, l.HWAddr) {
return l.IP
@@ -555,8 +648,8 @@ func (s *Server) FindIPbyMAC(mac net.HardwareAddr) net.IP {
// Reset internal state
func (s *Server) reset() {
- s.Lock()
+ s.leasesLock.Lock()
s.leases = nil
- s.Unlock()
+ s.leasesLock.Unlock()
s.IPpool = make(map[[4]byte]net.HardwareAddr)
}
diff --git a/helpers.go b/helpers.go
index 6f35caba..4d4d0b3f 100644
--- a/helpers.go
+++ b/helpers.go
@@ -2,6 +2,7 @@ package main
import (
"bufio"
+ "bytes"
"context"
"errors"
"fmt"
@@ -387,3 +388,18 @@ func _Func() string {
f := runtime.FuncForPC(pc[0])
return path.Base(f.Name())
}
+
+// Parse input string and return IPv4 address
+func parseIPv4(s string) net.IP {
+ ip := net.ParseIP(s)
+ if ip == nil {
+ return nil
+ }
+
+ v4InV6Prefix := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}
+ if !bytes.Equal(ip[0:12], v4InV6Prefix) {
+ return nil
+ }
+
+ return ip.To4()
+}
diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml
index 83856cbe..318849b0 100644
--- a/openapi/openapi.yaml
+++ b/openapi/openapi.yaml
@@ -387,6 +387,42 @@ paths:
schema:
$ref: "#/definitions/DhcpSearchResult"
+ /dhcp/add_static_lease:
+ post:
+ tags:
+ - dhcp
+ operationId: dhcpAddStaticLease
+ summary: "Adds a static lease"
+ consumes:
+ - application/json
+ parameters:
+ - in: "body"
+ name: "body"
+ required: true
+ schema:
+ $ref: "#/definitions/DhcpStaticLease"
+ responses:
+ 200:
+ description: OK
+
+ /dhcp/remove_static_lease:
+ post:
+ tags:
+ - dhcp
+ operationId: dhcpRemoveStaticLease
+ summary: "Removes a static lease"
+ consumes:
+ - application/json
+ parameters:
+ - in: "body"
+ name: "body"
+ required: true
+ schema:
+ $ref: "#/definitions/DhcpStaticLease"
+ responses:
+ 200:
+ description: OK
+
# --------------------------------------------------
# Filtering status methods
# --------------------------------------------------
@@ -1152,7 +1188,7 @@ definitions:
properties:
mac:
type: "string"
- example: "001109b3b3b8"
+ example: "00:11:09:b3:b3:b8"
ip:
type: "string"
example: "192.168.1.22"
@@ -1163,6 +1199,24 @@ definitions:
type: "string"
format: "date-time"
example: "2017-07-21T17:32:28Z"
+ DhcpStaticLease:
+ type: "object"
+ description: "DHCP static lease information"
+ required:
+ - "mac"
+ - "ip"
+ - "hostname"
+ - "expires"
+ properties:
+ mac:
+ type: "string"
+ example: "00:11:09:b3:b3:b8"
+ ip:
+ type: "string"
+ example: "192.168.1.22"
+ hostname:
+ type: "string"
+ example: "dell"
DhcpStatus:
type: "object"
description: "Built-in DHCP server configuration and status"
@@ -1176,6 +1230,10 @@ definitions:
type: "array"
items:
$ref: "#/definitions/DhcpLease"
+ static_leases :
+ type: "array"
+ items:
+ $ref: "#/definitions/DhcpStaticLease"
DhcpSearchResult:
type: "object"
description: "Information about a DHCP server discovered in the current network"