├── .github └── workflows │ ├── build.yml │ └── lint.yml ├── .gitignore ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── config.json ├── config_provider.go ├── docs ├── dhcplb-fb-deployment.graffle │ ├── data.plist │ └── image4.pdf ├── dhcplb-fb-deployment.jpg ├── extending-dhcplb.md └── getting-started.md ├── glog_logger.go ├── go.mod ├── go.sum ├── hosts-v4.txt ├── hosts-v6.txt ├── lib ├── config.go ├── dhcp_server.go ├── filesourcer.go ├── handler.go ├── interface.go ├── log.go ├── modulo.go ├── modulo_test.go ├── rr.go ├── rr_test.go ├── server.go ├── throttle.go ├── throttle_test.go ├── update_servers.go └── update_servers_test.go ├── main.go ├── overrides.json └── vagrant ├── README.md ├── Vagrantfile └── chef ├── cookbooks ├── .gitignore ├── .kitchen.yml ├── Berksfile ├── dhcpclient │ ├── README.md │ ├── metadata.rb │ └── recipes │ │ └── default.rb ├── dhcplb │ ├── README.md │ ├── files │ │ └── default │ │ │ └── dhcplb.config.json │ ├── metadata.rb │ ├── recipes │ │ └── default.rb │ └── templates │ │ └── default │ │ └── dhcp-servers-v4.cfg.erb ├── dhcprelay │ ├── README.md │ ├── metadata.rb │ ├── recipes │ │ └── default.rb │ └── templates │ │ └── default │ │ └── etc_default_isc-dhcp-relay.erb └── dhcpserver │ ├── README.md │ ├── files │ └── default │ │ └── etc_default_isc-dhcp-server │ ├── metadata.rb │ ├── recipes │ └── default.rb │ └── templates │ └── default │ └── dhcpd.conf.erb └── roles ├── base.rb ├── dhcpclient.rb ├── dhcplb.rb ├── dhcprelay.rb └── dhcpserver.rb /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-go@v3 13 | with: 14 | go-version: 1.19 15 | - name: Fetch Dependencies 16 | run: go get -v ./... 17 | - name: Run Tests 18 | run: go test -v ./... 19 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | lint: 9 | name: Lint Test 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-go@v3 14 | with: 15 | go-version: 1.19 16 | - name: Run golangci-lint 17 | uses: golangci/golangci-lint-action@v3 18 | with: 19 | # TODO: fix lint 20 | args: -D errcheck 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | *.swp 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | * Vinnie Magro (Production Engineer Intern 2016) 2 | * Angelo Failla (Production Engineer) 3 | * Roman Gushchin (Production Engineer) 4 | * Mateusz Kaczanowski (Production Engineer) 5 | * Jake Bunce (Network Engineer) 6 | * Emre Cantimur (Production Engineer) 7 | * Andrea Barberio (Production Engineer) 8 | * Pablo Mazzini (Production Engineer) 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `dhcplb` 2 | We want to make contributing to this project as easy and transparent as 3 | possible. 4 | 5 | ## Pull Requests 6 | We actively welcome your pull requests. 7 | 8 | 1. Fork the repo and create your branch from `main`. 9 | 2. If you've added code that should be tested, add tests. 10 | 3. If you've changed APIs, update the documentation. 11 | 4. Ensure the test suite passes. 12 | 5. Make sure your code lints. 13 | 6. If you haven't already, complete the Contributor License Agreement ("CLA"). 14 | 15 | ## Contributor License Agreement ("CLA") 16 | In order to accept your pull request, we need you to submit a CLA. You only need 17 | to do this once to work on any of Facebook's open source projects. 18 | 19 | Complete your CLA here: 20 | 21 | ## Issues 22 | We use GitHub issues to track public bugs. Please ensure your description is 23 | clear and has sufficient instructions to be able to reproduce the issue. 24 | 25 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe 26 | disclosure of security bugs. In those cases, please go through the process 27 | outlined on that page and do not file a public issue. 28 | 29 | ## License 30 | By contributing to `dhcplb`, you agree that your contributions will be licensed 31 | under the LICENSE file in the root directory of this source tree. 32 | 33 | # I don't want to make a pull request! 34 | We love pull requests, but it's not necessary to write code to contribute. If 35 | for any reason you can't make a pull request (e.g. you just want to suggest us 36 | an improvement), let us know. 37 | [Create an issue](https://help.github.com/articles/creating-an-issue/) 38 | on the `dhcplb` issue tracker and we will review your request. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is dhcplb? 2 | 3 | `dhcplb` is Facebook's implementation of: 4 | * a DHCP v4/v6 relayer with load balancing capabilities 5 | * a DHCP v4/v6 server framework 6 | 7 | Both modes currently only support handling messages sent by a relayer which is 8 | unicast traffic. It doesn't support broadcast (v4) and multicast (v6) requests. 9 | Facebook currently uses it in production, and it's deployed at global scale 10 | across all of our data centers. 11 | It is based on [@insomniacslk](https://github.com/insomniacslk) [dhcp library](https://github.com/insomniacslk/dhcp). 12 | 13 | # Why did you do that? 14 | 15 | Facebook uses DHCP to provide network configuration to bare-metal machines at 16 | provisioning phase and to assign IPs to out-of-band interfaces. 17 | 18 | `dhcplb` was created because the previous infrastructure surrounding DHCP led 19 | to very unbalanced load across the DHCP servers in a region when simply using 20 | Anycast+ECMP alone (for example 1 server out of 10 would receive >65% of 21 | requests). 22 | 23 | Facebook's DHCP infrastructure was [presented at SRECon15 Ireland](https://www.usenix.org/conference/srecon15europe/program/presentation/failla). 24 | 25 | Later, support for making it responsible for serving dhcp requests (server mode) 26 | was added. This was done because having a single threaded application (ISC KEA) 27 | queuing up packets while doing backend calls to another services wasn't scaling 28 | well for us. 29 | 30 | # Why not use an existing load balancer? 31 | 32 | * All the relayer implementations available on the internet lack the load 33 | balancing functionality. 34 | * Having control of the code gives you the ability to: 35 | * perform A/B testing on new builds of our DHCP server 36 | * implement override mechanism 37 | * implement anything additional you need 38 | 39 | # Why not use an existing server? 40 | 41 | We needed a server implementation which allow us to have both: 42 | * Multithreaded design, to avoid blocking requests when doing backend calls 43 | * An interface to be able to call other services for getting the IP assignment, 44 | boot file url, etc. 45 | 46 | # How do you use `dhcplb` at Facebook? 47 | 48 | This picture shows how we have deployed `dhcplb` in our production 49 | infrastructure: 50 | 51 | ![DHCPLB deployed at Facebook](/docs/dhcplb-fb-deployment.jpg) 52 | 53 | TORs (Top of Rack switch) at Facebook run DHCP relayers, these relayers are 54 | responsible for relaying broadcast DHCP traffic (DISCOVERY and SOLICIT 55 | messages) originating within their racks to anycast VIPs, one DHCPv4 and one 56 | for DHCPv6. 57 | 58 | In a Cisco switch the configuration would look like this: 59 | 60 | ``` 61 | ip helper-address 10.127.255.67 62 | ipv6 dhcp relay destination 2401:db00:eef0:a67:: 63 | ``` 64 | 65 | We have a bunch of `dhcplb` [Tupperware](https://blog.docker.com/2014/07/dockercon-video-containerized-deployment-at-facebook/) instances in every region listening on 66 | those VIPs. 67 | They are responsible for received traffic relayed by TORs agents and load 68 | balancing them amongst the actual `dhcplb` servers distributed across clusters 69 | in that same region. 70 | 71 | Having 2 layers allows us to A/B test changes of the server implementation. 72 | 73 | The configuration for `dhcplb` consists of 3 files: 74 | 75 | * json config file: contains the main configuration for the server as explained in the [Getting Started](docs/getting-started.md) section 76 | * host lists file: contains a list of dhcp servers, one per line, those are the servers `dhcplb` will try to balance on 77 | * overrides file: a file containing per mac overrides. See the [Getting Started](docs/getting-started.md) section. 78 | 79 | # TODOs / future improvements 80 | 81 | `dhcplb` does not support relaying/responding broadcasted DHCPv4 DISCOVERY 82 | packets or DHCPv6 SOLICIT packets sent to `ff02::1:2` multicast address. We 83 | don't need this in our production environment but adding that support should be 84 | trivial though. 85 | 86 | TODOs and improvements are tracked [here](https://github.com/facebookincubator/dhcplb/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) 87 | 88 | PRs are welcome! 89 | 90 | # How does the packet path looks like? 91 | 92 | When operating in v4 `dhcplb` will relay relayed messages coming from other 93 | relayers (in our production network those are rack switches), the response from 94 | the server will be relayed back to the rack switches: 95 | 96 | ``` 97 | dhcp client <---> rsw relayer ---> dhcplb (relay) ---> dhcplb (server) 98 | ^ | 99 | | | 100 | +--------------------------------------+ 101 | ``` 102 | 103 | In DHCPv6 responses by the dhcp server will traverse the load balancer. 104 | 105 | # Installation 106 | 107 | To install `dhcplb` into `$GOPATH/bin/dhcplb`, simply run: 108 | 109 | ``` 110 | $ go install github.com/facebookincubator/dhcplb@latest 111 | ``` 112 | 113 | # Cloning 114 | 115 | If you wish to clone the repo you can do the following: 116 | 117 | ``` 118 | $ mkdir -p $GOPATH/src/github.com/facebookincubator 119 | $ cd $_ 120 | $ git clone https://github.com/facebookincubator/dhcplb 121 | $ go install github.com/facebookincubator/dhcplb 122 | ``` 123 | 124 | # Run unit tests 125 | 126 | You can run tests with: 127 | 128 | ``` 129 | $ cd $GOPATH/src/github.com/facebookincubator/dhcplb/lib 130 | $ go test 131 | ``` 132 | 133 | # Getting Started and extending `dhcplb` 134 | 135 | `dhcplb` can be run out of the box after compilation. 136 | 137 | To start immediately, you can run 138 | `sudo dhcplb -config config.json -version 6`. 139 | That will start the relay in v6 mode using the default configuration. 140 | 141 | Should you need to integrate `dhcplb` with your infrastructure please 142 | see [Extending DHCPLB](docs/extending-dhcplb.md). 143 | 144 | # Virtual lab for development and testing 145 | 146 | You can bring up a virtual lab using vagrant. This will replicate our production 147 | environment, you can spawn VMs containing various components like: 148 | 149 | * N instances of `ISC dhcpd` 150 | * An instance of `dhcplb` 151 | * An instance of `dhcrelay`, simulating a top of rack switch. 152 | * a VM where you can run `dhclient` or `ISC perfdhcp` 153 | 154 | All of that is managed by `vagrant` and `chef-solo` cookbooks. 155 | You can use this lab to test your `dhcplb` changes. 156 | For more information have a look at the [vagrant directory](vagrant/README.md). 157 | 158 | # Who wrote it? 159 | 160 | `dhcplb` started in April 2016 during a 3 days hackathon in the Facebook 161 | Dublin office, the hackathon project proved the feasibility of the tool. 162 | In June we were joined by Vinnie Magro (@vmagro) for a 3 months internship in 163 | which he worked with two production engineers on turning the hack into a 164 | production ready system. 165 | 166 | Original Hackathon project members: 167 | 168 | * Angelo Failla ([@pallotron](https://github.com/pallotron)), Production Engineer 169 | * Roman Gushchin ([@rgushchin](https://github.com/rgushchin)), Production Engineer 170 | * Mateusz Kaczanowski ([@mkaczanowski](https://github.com/mkaczanowski)), Production Engineer 171 | * Jake Bunce, Network Engineer 172 | 173 | Internship project members: 174 | 175 | * Vinnie Magro ([@vmagro](https://github.com/vmagro)), Production Engineer intern 176 | * Angelo Failla (@pallotron), Intern mentor, Production Engineer 177 | * Mateusz Kaczanowski (@mkaczanowski), Production Engineer 178 | 179 | Other contributors: 180 | 181 | * Emre Cantimur, Production Engineer, Facebook, Throttling support 182 | * Andrea Barberio, Production Engineer, Facebook 183 | * Pablo Mazzini, Production Engineer, Facebook 184 | 185 | # License 186 | 187 | BSD License. See the LICENSE file. 188 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "v4": { 3 | "version": 4, 4 | "listen_addr": "0.0.0.0", 5 | "port": 67, 6 | "packet_buf_size": 1024, 7 | "update_server_interval": 30, 8 | "algorithm": "xid", 9 | "host_sourcer": "file:hosts-v4.txt", 10 | "rc_ratio": 0, 11 | "throttle_cache_size": 1024, 12 | "throttle_cache_rate": 128, 13 | "throttle_rate": 256 14 | }, 15 | "v6": { 16 | "version": 6, 17 | "listen_addr": "::", 18 | "port": 547, 19 | "packet_buf_size": 1024, 20 | "update_server_interval": 30, 21 | "algorithm": "xid", 22 | "host_sourcer": "file:hosts-v6.txt", 23 | "rc_ratio": 0, 24 | "throttle_cache_size": 1024, 25 | "throttle_cache_rate": 128, 26 | "throttle_rate": 256 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /config_provider.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package main 9 | 10 | import ( 11 | "encoding/json" 12 | 13 | "github.com/facebookincubator/dhcplb/lib" 14 | ) 15 | 16 | // DefaultConfigProvider holds configuration for the server. 17 | type DefaultConfigProvider struct{} 18 | 19 | // NewDefaultConfigProvider returns a new DefaultConfigProvider 20 | func NewDefaultConfigProvider() *DefaultConfigProvider { 21 | return &DefaultConfigProvider{} 22 | } 23 | 24 | // NewHostSourcer returns a dhcplb.DHCPServerSourcer interface. 25 | // The default config loader is able to instantiate a FileSourcer by itself, so 26 | // NewHostSourcer here will simply return (nil, nil). 27 | // The FileSourcer implemments dhcplb.DHCPServerSourcer interface. 28 | // If you are writing your own implementation of dhcplb you could write your 29 | // custom sourcer implementation here. 30 | // sourcerType 31 | // The NewHostSourcer function is passed values from the host_sourcer json 32 | // config option with the sourcerType being the part of the string before 33 | // the : and args the remaining portion. 34 | // ex: file:hosts-v4.txt,hosts-v4-rc.txt in the json config file will have 35 | // sourcerType="file" and args="hosts-v4.txt,hosts-v4-rc.txt". 36 | func (h DefaultConfigProvider) NewHostSourcer(sourcerType, args string, version int) (dhcplb.DHCPServerSourcer, error) { 37 | return nil, nil 38 | } 39 | 40 | // ParseExtras is used to return extra config. Here we return nil because we 41 | // don't need any extra configuration in the opensource version of dhcplb. 42 | func (h DefaultConfigProvider) ParseExtras(data json.RawMessage) (interface{}, error) { 43 | return nil, nil 44 | } 45 | 46 | // NewDHCPBalancingAlgorithm returns a DHCPBalancingAlgorithm implementation. 47 | // This can be used if you need to create your own balancing algorithm and 48 | // integrate it with your infra without necesarily having to realase your code 49 | // to github. 50 | func (h DefaultConfigProvider) NewDHCPBalancingAlgorithm(version int) (dhcplb.DHCPBalancingAlgorithm, error) { 51 | return nil, nil 52 | } 53 | 54 | // NewHandler takes an interface with extra configurations and returns a 55 | // Handler used for serving DHCP requests. It is only needed when using dhcplb 56 | // in server mode. 57 | func (h DefaultConfigProvider) NewHandler(extras interface{}, version int) (dhcplb.Handler, error) { 58 | return nil, nil 59 | } 60 | -------------------------------------------------------------------------------- /docs/dhcplb-fb-deployment.graffle/data.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookincubator/dhcplb/517055e43b1157c26fce7b75536beda95dce9d93/docs/dhcplb-fb-deployment.graffle/data.plist -------------------------------------------------------------------------------- /docs/dhcplb-fb-deployment.graffle/image4.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookincubator/dhcplb/517055e43b1157c26fce7b75536beda95dce9d93/docs/dhcplb-fb-deployment.graffle/image4.pdf -------------------------------------------------------------------------------- /docs/dhcplb-fb-deployment.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookincubator/dhcplb/517055e43b1157c26fce7b75536beda95dce9d93/docs/dhcplb-fb-deployment.jpg -------------------------------------------------------------------------------- /docs/extending-dhcplb.md: -------------------------------------------------------------------------------- 1 | # Extending DHCPLB 2 | 3 | It's possible to extend `dhcplb` to modify the way it fetches the list of 4 | DHCP servers, or have a different logging implementation, or add different 5 | balancing algorithm, or make it behave as a server, replying to requests 6 | directly. 7 | At the moment this is a bit complex but we will work on ways to make it easier. 8 | 9 | ## Adding a new balancing algorithm. 10 | 11 | Adding a new algorithm can be done by implementing something that matches 12 | the `DHCPBalancingAlgorithm` interface: 13 | 14 | ```go 15 | type DHCPBalancingAlgorithm interface { 16 | selectServerFromList(list []*DHCPServer, message *DHCPMessage) (*DHCPServer, error) 17 | selectRatioBasedDhcpServer(message *DHCPMessage) (*DHCPServer, error) 18 | updateStableServerList(list []*DHCPServer) error 19 | updateRCServerList(list []*DHCPServer) error 20 | setRCRatio(ratio uint32) 21 | Name() string 22 | } 23 | ``` 24 | 25 | Then add it to the `algorithms` map in the `configSpec.algorithm` function, in 26 | the `config.go` file. 27 | Do that if you want to share the algorithm with the community. 28 | 29 | If, however, you need to implement something that you can't share, because, for 30 | example, it's internal and specific to your infra, you can write something that 31 | implements the `ConfigProvider` interface, in particular the 32 | `NewDHCPBalancingAlgorithm` function. 33 | 34 | ## Adding more configuration options. 35 | 36 | More configuration options can be added to the config JSON file using the 37 | `ConfigProvider` interface: 38 | 39 | ```go 40 | type ConfigProvider interface { 41 | NewHostSourcer(sourcerType, args string, version int) (DHCPServerSourcer, error) 42 | ParseExtras(extras json.RawMessage) (interface{}, error) 43 | NewDHCPBalancingAlgorithm(version int) (DHCPBalancingAlgorithm, error) 44 | NewHandler(extras interface{}, version int) (Handler, error) 45 | } 46 | ``` 47 | 48 | The `NewHostSourcer` function is passed values from the `host_sourcer` config option 49 | with the `sourcerType` being the part of the string before the `:` and `args` the 50 | remaining portion. ex: `file:hosts-v4.txt,hosts-v4-rc.txt` will have `sourcerType="file"` 51 | and `args="hosts-v4.txt,hosts-v4-rc-txt"`. 52 | The default `Config` loader is able to instantiate a `FileSourcer` by itself, so 53 | `NewHostSourcer` can simply return `nil, nil` unless you are using a custom sourcer 54 | implementation. 55 | 56 | Any struct can be returned from the `ParseExtras` function and used elsewhere in 57 | the code via the `Extras` member of a `Config` struct. 58 | 59 | As mentioned in the section before `NewDHCPBalancingAlgorithm` can be used 60 | to return your own specific load balancing implementation. 61 | 62 | ## Write your own logic to source list of DHCP servers 63 | 64 | If you want to change the way `dhcplb` sources the list of DHCP servers (for 65 | example you want to source them from a backend system like a database) you can 66 | have something implementing the `DHCPServerSourcer` interface: 67 | 68 | ```go 69 | type DHCPServerSourcer interface { 70 | GetStableServers() ([]*DHCPServer, error) 71 | GetRCServers() ([]*DHCPServer, error) 72 | // get servers from a specific named group (this is used with overrides) 73 | GetServersFromTier(tier string) ([]*DHCPServer, error) 74 | } 75 | ``` 76 | 77 | Then implement your own `ConfigProvider` interface and make it return a 78 | `DHCPServerSourcer`. Then in the main you can replace `NewDefaultConfigProvider` 79 | with your own `ConfigProvider` implementation. 80 | 81 | ## Write your own server handler 82 | 83 | If you want to make `dhcplb` responsible for serving dhcp requests you can implement 84 | the `Handler` interface. The methods of the interface take an incoming packet and 85 | return the crafted response the server is going to reply with. 86 | 87 | ```go 88 | type Handler interface { 89 | ServeDHCPv4(packet *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, error) 90 | ServeDHCPv6(packet dhcpv6.DHCPv6) (dhcpv6.DHCPv6, error) 91 | } 92 | ``` 93 | 94 | Then implement your own `ConfigProvider` interface and make it return a `Handler` 95 | interface. `dhcplb` should be started in server mode using the `-server` flag. 96 | 97 | When creating a `Handler`, the `Extra` configuration options are passed to it, so 98 | things such as DNS, NTP servers or lease time can be defined there. 99 | 100 | When `dhcplb` is used to serve requests directly, the `DHCPServerSourcer` and 101 | `DHCPBalancingAlgorithm` interfaces are not used. 102 | 103 | ### Example 104 | 105 | Define a config provider with its methods and the extra configuration options. 106 | 107 | ```go 108 | // MyConfigProvider implements the ConfigProvider interface 109 | type MyConfigProvider struct { 110 | } 111 | 112 | // MyConfigExtras represents extra configuration options 113 | type MyConfigExtras struct { 114 | NameServers []string `json:"name_servers"` 115 | LeaseTime uint32 `json:"lease_time_s"` 116 | } 117 | 118 | // ParseExtras is responsible for parsing extra configuration options 119 | func (p MyConfigProvider) ParseExtras(extrasJSON json.RawMessage) (interface{}, error) { 120 | var extras MyConfigExtras 121 | if err := json.Unmarshal(extrasJSON, &extras); err != nil { 122 | return nil, fmt.Errorf("Error parsing extras JSON: %s", err) 123 | } 124 | return extras, nil 125 | } 126 | 127 | // NewHandler returns the handler for serving DHCP requests 128 | func (p MyConfigProvider) NewHandler(extras interface{}, version int) (Handler, error) { 129 | config, ok := extras.(MyConfigExtras) 130 | if !ok { 131 | return nil, fmt.Errorf("MyConfigExtras type assertion error") 132 | } 133 | return &MyHandler{config: config}, nil 134 | } 135 | ``` 136 | 137 | Define a server handler and its methods. 138 | 139 | ```go 140 | // MyHandler contains data needed to handle DHCP requests 141 | type MyHandler struct { 142 | config MyConfigExtras 143 | } 144 | 145 | // ServeDHCPv6 handles DHCPv6 requests 146 | func (h MyHandler) ServeDHCPv6(packet dhcpv6.DHCPv6) (dhcpv6.DHCPv6, error) { 147 | msg, err := packet.GetInnerMessage() 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | mac, err := dhcpv6.ExtractMAC(packet) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | reply, err := buildReply(msg) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | ... 163 | 164 | var nameservers []net.IP 165 | for _, ns := range h.config.NameServers { 166 | nameservers = append(nameservers, net.ParseIP(ns)) 167 | } 168 | reply.AddOption(&dhcpv6.OptDNSRecursiveNameServer{NameServers: nameservers}) 169 | 170 | ... 171 | 172 | return reply, nil 173 | } 174 | ``` 175 | 176 | Create a configuration file and watch the config changes. 177 | 178 | ``` 179 | { 180 | "v6": { 181 | "version": 6, 182 | "listen_addr": "::", 183 | "port": 547, 184 | "packet_buf_size": 1024, 185 | "update_server_interval": 30, 186 | "algorithm": "xid", 187 | "host_sourcer": "file:hosts-v6.txt", 188 | "rc_ratio": 0, 189 | "throttle_cache_size": 1024, 190 | "throttle_cache_rate": 128, 191 | "throttle_rate": 256, 192 | "extras": { 193 | "name_servers": [ 194 | "2001:4860:4860::8888", 195 | "2001:4860:4860::8844" 196 | ], 197 | "lease_time_s": 43200 198 | } 199 | } 200 | } 201 | ``` 202 | 203 | ```go 204 | configChan, err := dhcplb.WatchConfig( 205 | *configPath, *overridesPath, *version, &MyConfigProvider{}) 206 | ``` 207 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Out of the box `dhcplb` supports loading DHCP server lists from text files and logging to stderr with `glog`. 4 | All configuration files supplied to `dhcplb` (config, overrides and DHCP server files) are watched for changes using [`fsnotify`](https://github.com/fsnotify/fsnotify) and hot-reloaded without restarting the server. 5 | Configuration is provided to the program via a JSON file 6 | 7 | ```javascript 8 | { 9 | "v4": { 10 | "version": 4, // DHCP operation mode 11 | "listen_addr": "0.0.0.0", // address to bind the receiving socket to 12 | "port": 67, // port to listen on 13 | "packet_buf_size": 1024, // size of buffer to allocate for incoming packet 14 | "update_server_interval": 30, // how often to refresh server list (in seconds) 15 | "algorithm": "xid", // balancing algorithm, supported are xid and rr (client hash and roundrobin) 16 | "host_sourcer": "file:hosts-v4.txt", // load DHCP server list from hosts-v4.txt 17 | "rc_ratio": 0, // what percentage of requests should go to RC servers 18 | "throttle_cache_size": 1024, // cache size for number of throttling objects for unique clients 19 | "throttle_cache_rate": 128, // rate value for throttling cache invalidation (per second) 20 | "throttle_rate": 256 // rate value for request per second 21 | }, 22 | ... (same options for "v6") ... 23 | ``` 24 | 25 | ## Overrides 26 | 27 | `dhcplb` supports configurable overrides for individual machines. A MAC address 28 | can be configured to point to a specific DHCP server IP or to a "tier" (group) 29 | of servers. 30 | Overrides are defined in a JSON file and the path is passed to `dhcplb` as the 31 | command-line arg `-overrides`. 32 | 33 | ```javascript 34 | { 35 | "v4": { 36 | "12:34:56:78:90:ab": { 37 | "host": "173.252.90.132" 38 | }, 39 | "fe:dc:ba:09:87:65": { 40 | "tier": "myGroup" 41 | } 42 | }, 43 | "v6": { 44 | } 45 | } 46 | ``` 47 | 48 | With this overrides file, DHCPv4 requests coming from MAC `12:34:56:78:90:ab` 49 | will be sent to the DHCP server at `173.252.90.132`, and requests from MAC 50 | `fe:dc:ba:09:87:65` will be sent to the tier of servers `myGroup` (a server will 51 | be picked according to the balancing algorithm's selection from the list of 52 | servers returned by the `GetServersFromTier(tier string)` function of the 53 | `DHCPServerSourcer` being used). 54 | Overrides may be associated with an expiration timestamp in the form 55 | "YYYY/MM/DD HH:MM TIMEZONE_OFFSET", where TIMEZONE_OFFSET is 56 | the timezone offset with respect to UTC. `dhcplb` will convert the timestamp 57 | in the local timezone and ignore expired overrides. 58 | 59 | ```javascript 60 | { 61 | "v4": { 62 | "12:34:56:78:90:ab": { 63 | "host": "173.252.90.132", 64 | "expiration": "2017/05/06 14:00 +0000" 65 | }, 66 | "fe:dc:ba:09:87:65": { 67 | "tier": "myGroup" 68 | } 69 | }, 70 | "v6": { 71 | } 72 | } 73 | ``` 74 | 75 | ## Throttling 76 | 77 | `dhcplb` keeps track of the request rate per second for each backend DHCP 78 | server. 79 | It can be set through `throttle_rate` configuration parameter. 80 | Requests exceeding this limit will be logged and dropped. For 0 or negative 81 | values no throttling will be done, and no cache will be created. 82 | 83 | An LRU cache is used to keep track of rate information for each backend DHCP 84 | server. 85 | Cache size can be set through `throttle_cache_size`. To prevent fast cache 86 | invalidation from malicious clients, `dhcplb` also keeps track of the number of 87 | new clients being added to the cache (per second). This behavior can be set 88 | through `throttle_cache_rate` configuration parameter. For 0 or negative values 89 | no cache rate limiting will be done. 90 | 91 | ## A/B testing 92 | 93 | `dhcplb` supports sending a percentage of requests to servers marked as RC and 94 | the rest to Stable servers. 95 | This percentage is configurable via the `rc_ratio` JSON option. 96 | Using the A/B testing functionality requires providing two lists of servers, 97 | this can be done via the built in filesourcer by specifying the `host_sourcer` 98 | option as `"file:,"` 99 | 100 | ## Usage 101 | 102 | ``` 103 | $ ./dhcplb -h 104 | Usage of ./dhcplb: 105 | -alsologtostderr 106 | log to standard error as well as files 107 | -config string 108 | Path to JSON config file 109 | -log_backtrace_at value 110 | when logging hits line file:N, emit a stack trace 111 | -log_dir string 112 | If non-empty, write log files in this directory 113 | -logtostderr 114 | log to standard error instead of files 115 | -overrides string 116 | Path to JSON overrides file 117 | -pprof int 118 | Port to run pprof HTTP server on 119 | -server 120 | Run in server mode. The default is relay mode. 121 | -stderrthreshold value 122 | logs at or above this threshold go to stderr 123 | -v value 124 | log level for V logs 125 | -version int 126 | Run in v4/v6 mode (default 4) 127 | -vmodule value 128 | comma-separated list of pattern=N settings for file-filtered logging 129 | ``` 130 | -------------------------------------------------------------------------------- /glog_logger.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package main 9 | 10 | import ( 11 | "fmt" 12 | "net" 13 | "sort" 14 | "strings" 15 | 16 | dhcplb "github.com/facebookincubator/dhcplb/lib" 17 | "github.com/golang/glog" 18 | "github.com/insomniacslk/dhcp/dhcpv4" 19 | "github.com/insomniacslk/dhcp/dhcpv6" 20 | ) 21 | 22 | type glogLogger struct{} 23 | 24 | // NewGlogLogger returns a glogLogger struct based on the 25 | // dhcplb.PersonalizedLogger interface. 26 | func NewGlogLogger() dhcplb.PersonalizedLogger { 27 | return glogLogger{} 28 | } 29 | 30 | // Log takes a dhcplb.LogMessage, creates a sample map[string] containing 31 | // information about the served request and prints it to stdout/err. 32 | func (l glogLogger) Log(msg dhcplb.LogMessage) error { 33 | sample := map[string]interface{}{ 34 | "version": msg.Version, 35 | "dhcp_server": msg.Server, 36 | "server_is_rc": msg.ServerIsRC, 37 | "source_ip": msg.Peer.IP.String(), 38 | "success": msg.Success, 39 | "latency_us": msg.Latency.Nanoseconds() / 1000, 40 | } 41 | if msg.ErrorName != "" { 42 | sample["error_name"] = msg.ErrorName 43 | sample["error_details"] = fmt.Sprintf("%s", msg.ErrorDetails) 44 | } 45 | 46 | if msg.Packet != nil { 47 | if msg.Version == 4 { 48 | packet, err := dhcpv4.FromBytes(msg.Packet) 49 | if err != nil { 50 | glog.Errorf("Error decoding DHCPv4 packet: %s", err) 51 | return err 52 | } 53 | sample["type"] = packet.MessageType().String() 54 | sample["xid"] = packet.TransactionID.String() 55 | sample["giaddr"] = packet.GatewayIPAddr.String() 56 | sample["client_mac"] = packet.ClientHWAddr.String() 57 | } else if msg.Version == 6 { 58 | packet, err := dhcpv6.FromBytes(msg.Packet) 59 | if err != nil { 60 | glog.Errorf("Error decoding DHCPv6 packet: %s", err) 61 | return err 62 | } 63 | sample["type"] = packet.Type().String() 64 | msg, err := packet.GetInnerMessage() 65 | if err != nil { 66 | glog.Errorf("Failed to get inner packet: %s", err) 67 | return err 68 | } 69 | sample["xid"] = msg.TransactionID.String() 70 | if duid := msg.Options.ClientID(); duid != nil { 71 | sample["duid"] = net.HardwareAddr(duid.ToBytes()).String() 72 | sample["duid_type"] = duid.DUIDType().String() 73 | } 74 | if mac, err := dhcpv6.ExtractMAC(packet); err != nil { 75 | glog.Errorf("error getting mac: %s", err) 76 | } else { 77 | sample["client_mac"] = mac.String() 78 | } 79 | if packet.IsRelay() { 80 | relay := packet.(*dhcpv6.RelayMessage) 81 | sample["link-addr"] = relay.LinkAddr.String() 82 | sample["peer-addr"] = relay.PeerAddr.String() 83 | } 84 | } 85 | } 86 | 87 | // Order samples by key, store them into logline slice 88 | keys := make([]string, len(sample)) 89 | i := 0 90 | for key := range sample { 91 | keys[i] = key 92 | i++ 93 | } 94 | sort.Strings(keys) 95 | logline := make([]string, len(sample)) 96 | i = 0 97 | for k := range keys { 98 | logline[i] = fmt.Sprintf("%s: %+v", keys[k], sample[keys[k]]) 99 | i++ 100 | } 101 | 102 | if msg.Success { 103 | glog.Infof("%s", strings.Join(logline, ", ")) 104 | } else { 105 | glog.Errorf("%s", strings.Join(logline, ", ")) 106 | } 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/facebookincubator/dhcplb 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.6.0 7 | github.com/golang/glog v1.2.4 8 | github.com/hashicorp/golang-lru/v2 v2.0.2 9 | github.com/insomniacslk/dhcp v0.0.0-20230307103557-e252950ab961 10 | golang.org/x/time v0.3.0 11 | ) 12 | 13 | require ( 14 | github.com/josharian/native v1.1.0 // indirect 15 | github.com/pierrec/lz4/v4 v4.1.14 // indirect 16 | github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect 17 | golang.org/x/sys v0.5.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 3 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 4 | github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= 5 | github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= 6 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 7 | github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5twqnfBdU= 8 | github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 9 | github.com/insomniacslk/dhcp v0.0.0-20230307103557-e252950ab961 h1:x/YtdDlmypenG1te/FfH6LVM+3krhXk5CFV8VYNNX5M= 10 | github.com/insomniacslk/dhcp v0.0.0-20230307103557-e252950ab961/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI= 11 | github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 12 | github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= 13 | github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 14 | github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= 15 | github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 17 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 18 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 19 | github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= 20 | github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= 21 | golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 24 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 26 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 27 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 28 | -------------------------------------------------------------------------------- /hosts-v4.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookincubator/dhcplb/517055e43b1157c26fce7b75536beda95dce9d93/hosts-v4.txt -------------------------------------------------------------------------------- /hosts-v6.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookincubator/dhcplb/517055e43b1157c26fce7b75536beda95dce9d93/hosts-v6.txt -------------------------------------------------------------------------------- /lib/config.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | "net" 14 | "os" 15 | "path/filepath" 16 | "strings" 17 | "time" 18 | 19 | "github.com/fsnotify/fsnotify" 20 | "github.com/golang/glog" 21 | ) 22 | 23 | // ConfigProvider is an interface which provides methods to fetch the 24 | // HostSourcer, parse extra configuration, provide additional load balancing 25 | // implementations and how to handle dhcp requests in server mode 26 | type ConfigProvider interface { 27 | NewHostSourcer( 28 | sourcerType, args string, version int) (DHCPServerSourcer, error) 29 | ParseExtras(extras json.RawMessage) (interface{}, error) 30 | NewDHCPBalancingAlgorithm(version int) (DHCPBalancingAlgorithm, error) 31 | NewHandler(extras interface{}, version int) (Handler, error) 32 | } 33 | 34 | // Config represents the server configuration. 35 | type Config struct { 36 | Version int 37 | Addr *net.UDPAddr 38 | Algorithm DHCPBalancingAlgorithm 39 | ServerUpdateInterval time.Duration 40 | PacketBufSize int 41 | Handler Handler 42 | HostSourcer DHCPServerSourcer 43 | RCRatio uint32 44 | Overrides map[string]Override 45 | Extras interface{} 46 | CacheSize int 47 | CacheRate int 48 | Rate int 49 | ReplyAddr *net.UDPAddr 50 | } 51 | 52 | // Override represents the dhcp server or the group of dhcp servers (tier) we 53 | // want to send packets to. 54 | type Override struct { 55 | // note that Host override takes precedence over Tier 56 | Host string `json:"host"` 57 | Tier string `json:"tier"` 58 | Expiration string `json:"expiration"` 59 | } 60 | 61 | // Overrides is a struct that holds v4 and v6 list of overrides. 62 | // The keys of the map are mac addresses. 63 | type Overrides struct { 64 | V4 map[string]Override `json:"v4"` 65 | V6 map[string]Override `json:"v6"` 66 | } 67 | 68 | // LoadConfig will take the path of the json file, the path of the override json 69 | // file, an integer version and a ConfigProvider and will return a pointer to 70 | // a Config object. 71 | func LoadConfig(path, overridesPath string, version int, provider ConfigProvider) (*Config, error) { 72 | file, err := os.ReadFile(path) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | overridesFile := []byte{} 78 | // path length of 0 means we aren't using overrides 79 | if len(overridesPath) != 0 { 80 | if overridesFile, err = os.ReadFile(overridesPath); err != nil { 81 | return nil, err 82 | } 83 | } 84 | return ParseConfig(file, overridesFile, version, provider) 85 | } 86 | 87 | // ParseConfig will take JSON config files, a version and a ConfigProvider, 88 | // and return a pointer to a Config struct 89 | func ParseConfig(jsonConfig, jsonOverrides []byte, version int, provider ConfigProvider) (*Config, error) { 90 | var combined combinedconfigSpec 91 | if err := json.Unmarshal(jsonConfig, &combined); err != nil { 92 | glog.Errorf("Failed to parse JSON: %s", err) 93 | return nil, err 94 | } 95 | var spec configSpec 96 | if version == 4 { 97 | spec = combined.V4 98 | } else if version == 6 { 99 | spec = combined.V6 100 | } 101 | 102 | var overrides map[string]Override 103 | if len(jsonOverrides) == 0 { 104 | overrides = make(map[string]Override) 105 | } else { 106 | var err error 107 | overrides, err = parseOverrides(jsonOverrides, version) 108 | if err != nil { 109 | glog.Errorf("Failed to load overrides: %s", err) 110 | return nil, err 111 | } 112 | } 113 | glog.Infof("Loaded %d override(s)", len(overrides)) 114 | return newConfig(&spec, overrides, provider) 115 | } 116 | 117 | // WatchConfig will keep watching for changes to both config and override json 118 | // files. It uses fsnotify library (it uses inotify in Linux), and call 119 | // LoadConfig when it an inotify event signals the modification of the json 120 | // files. 121 | func WatchConfig( 122 | configPath, overridesPath string, version int, provider ConfigProvider, 123 | ) (chan *Config, error) { 124 | configChan := make(chan *Config) 125 | 126 | watcher, err := fsnotify.NewWatcher() 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | // strings containing the real path of a config files, if they are symlinks 132 | var realConfigPath string 133 | var realOverridesPath string 134 | 135 | err = watcher.Add(filepath.Dir(configPath)) 136 | if err != nil { 137 | return nil, err 138 | } 139 | realConfigPath, err = filepath.EvalSymlinks(configPath) 140 | if err == nil { 141 | // configPath is a symlink, also watch the pointee 142 | err = watcher.Add(realConfigPath) 143 | if err != nil { 144 | return nil, err 145 | } 146 | } 147 | 148 | // setup watcher on overrides file if present 149 | if len(overridesPath) > 0 { 150 | err = watcher.Add(filepath.Dir(overridesPath)) 151 | if err != nil { 152 | return nil, err 153 | } 154 | realOverridesPath, err = filepath.EvalSymlinks(overridesPath) 155 | if err == nil { 156 | // overridesPath is a symlink, also watch the pointee 157 | err = watcher.Add(realOverridesPath) 158 | if err != nil { 159 | return nil, err 160 | } 161 | } 162 | } 163 | 164 | // watch for fsnotify events 165 | go func() { 166 | for { 167 | select { 168 | case ev := <-watcher.Events: 169 | // ignore Remove events 170 | if ev.Op&fsnotify.Remove == fsnotify.Remove { 171 | continue 172 | } 173 | // only care about symlinks and target of symlinks 174 | if ev.Name == overridesPath || ev.Name == configPath || 175 | ev.Name == realOverridesPath || ev.Name == realConfigPath { 176 | glog.Infof("Configuration file changed (%s), reloading", ev) 177 | config, err := LoadConfig( 178 | configPath, overridesPath, version, provider) 179 | if err != nil { 180 | glog.Fatalf("Failed to reload config: %s", err) 181 | panic(err) // fail hard 182 | } 183 | configChan <- config 184 | } 185 | case err := <-watcher.Errors: 186 | glog.Errorf("fsnotify error: %s", err) 187 | } 188 | } 189 | }() 190 | 191 | return configChan, nil 192 | } 193 | 194 | // configSpec holds the raw json configuration. 195 | type configSpec struct { 196 | Path string 197 | Version int `json:"version"` 198 | ListenAddr string `json:"listen_addr"` 199 | Port int `json:"port"` 200 | AlgorithmName string `json:"algorithm"` 201 | UpdateServerInterval int `json:"update_server_interval"` 202 | PacketBufSize int `json:"packet_buf_size"` 203 | HostSourcer string `json:"host_sourcer"` 204 | RCRatio uint32 `json:"rc_ratio"` 205 | Extras json.RawMessage `json:"extras"` 206 | CacheSize int `json:"throttle_cache_size"` 207 | CacheRate int `json:"throttle_cache_rate"` 208 | Rate int `json:"throttle_rate"` 209 | ReplyAddr string `json:"reply_addr"` 210 | } 211 | 212 | type combinedconfigSpec struct { 213 | V4 configSpec `json:"v4"` 214 | V6 configSpec `json:"v6"` 215 | } 216 | 217 | func (c *configSpec) sourcer(provider ConfigProvider) (DHCPServerSourcer, error) { 218 | // Load the DHCPServerSourcer implementation 219 | sourcerInfo := strings.Split(c.HostSourcer, ":") 220 | sourcerType := sourcerInfo[0] 221 | stable := sourcerInfo[1] 222 | rc := "" 223 | if strings.Contains(sourcerInfo[1], ",") { 224 | sourcerArgs := strings.Split(sourcerInfo[1], ",") 225 | stable = sourcerArgs[0] 226 | rc = sourcerArgs[1] 227 | } 228 | switch sourcerType { 229 | 230 | default: 231 | return provider.NewHostSourcer(sourcerType, sourcerInfo[1], c.Version) 232 | 233 | case "file": 234 | sourcer, err := NewFileSourcer(stable, rc, c.Version) 235 | if err != nil { 236 | glog.Fatalf("Can't load FileSourcer") 237 | } 238 | return sourcer, err 239 | } 240 | } 241 | 242 | func (c *configSpec) algorithm(provider ConfigProvider) (DHCPBalancingAlgorithm, error) { 243 | // Balancing algorithms coming with the dhcplb source code 244 | modulo := new(modulo) 245 | rr := new(roundRobin) 246 | algorithms := map[string]DHCPBalancingAlgorithm{ 247 | modulo.Name(): modulo, 248 | rr.Name(): rr, 249 | } 250 | // load other non default algorithms from the ConfigProvider 251 | providedAlgo, err := provider.NewDHCPBalancingAlgorithm(c.Version) 252 | if err != nil { 253 | glog.Fatalf("Provided load balancing implementation error: %s", err) 254 | } 255 | if providedAlgo != nil { 256 | if _, exists := algorithms[providedAlgo.Name()]; exists { 257 | glog.Fatalf("Algorithm name %s exists already, pick another name.", providedAlgo.Name()) 258 | 259 | } 260 | algorithms[providedAlgo.Name()] = providedAlgo 261 | } 262 | lb, ok := algorithms[c.AlgorithmName] 263 | if !ok { 264 | supported := []string{} 265 | for k := range algorithms { 266 | supported = append(supported, k) 267 | } 268 | glog.Fatalf( 269 | "'%s' is not a supported balancing algorithm. "+ 270 | "Supported balancing algorithms are: %v", 271 | c.AlgorithmName, supported) 272 | return nil, fmt.Errorf( 273 | "'%s' is not a supported balancing algorithm", c.AlgorithmName) 274 | } 275 | lb.SetRCRatio(c.RCRatio) 276 | return lb, nil 277 | } 278 | 279 | func newConfig(spec *configSpec, overrides map[string]Override, provider ConfigProvider) (*Config, error) { 280 | if spec.Version != 4 && spec.Version != 6 { 281 | return nil, fmt.Errorf("Supported version: 4, 6 - not %d", spec.Version) 282 | } 283 | 284 | targetIP := net.ParseIP(spec.ListenAddr) 285 | if targetIP == nil { 286 | return nil, fmt.Errorf("Unable to parse IP %s", targetIP) 287 | } 288 | addr := &net.UDPAddr{ 289 | IP: targetIP, 290 | Port: spec.Port, 291 | Zone: "", 292 | } 293 | 294 | algo, err := spec.algorithm(provider) 295 | if err != nil { 296 | return nil, err 297 | } 298 | sourcer, err := spec.sourcer(provider) 299 | if err != nil { 300 | return nil, err 301 | } 302 | 303 | // extras 304 | extras, err := provider.ParseExtras(spec.Extras) 305 | if err != nil { 306 | return nil, err 307 | } 308 | handler, err := provider.NewHandler(extras, spec.Version) 309 | if err != nil { 310 | return nil, err 311 | } 312 | 313 | return &Config{ 314 | Version: spec.Version, 315 | Addr: addr, 316 | Algorithm: algo, 317 | ServerUpdateInterval: time.Duration( 318 | spec.UpdateServerInterval) * time.Second, 319 | PacketBufSize: spec.PacketBufSize, 320 | Handler: handler, 321 | HostSourcer: sourcer, 322 | RCRatio: spec.RCRatio, 323 | Overrides: overrides, 324 | Extras: extras, 325 | CacheSize: spec.CacheSize, 326 | CacheRate: spec.CacheRate, 327 | Rate: spec.Rate, 328 | ReplyAddr: &net.UDPAddr{IP: net.ParseIP(spec.ReplyAddr)}, 329 | }, nil 330 | } 331 | 332 | func parseOverrides(file []byte, version int) (map[string]Override, error) { 333 | overrides := Overrides{} 334 | err := json.Unmarshal(file, &overrides) 335 | if err != nil { 336 | glog.Errorf("Failed to parse JSON: %s", err) 337 | return nil, err 338 | } 339 | if version == 4 { 340 | return overrides.V4, nil 341 | } else if version == 6 { 342 | return overrides.V6, nil 343 | } 344 | return nil, fmt.Errorf("Unsupported version %d, must be 4|6", version) 345 | } 346 | -------------------------------------------------------------------------------- /lib/dhcp_server.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "fmt" 12 | "net" 13 | ) 14 | 15 | // DHCPServer holds information about a single dhcp server 16 | type DHCPServer struct { 17 | Hostname string 18 | Address net.IP 19 | Port int 20 | IsRC bool 21 | } 22 | 23 | // NewDHCPServer returns an instance of DHCPServer 24 | func NewDHCPServer(hostname string, ip net.IP, port int) *DHCPServer { 25 | s := DHCPServer{ 26 | Hostname: hostname, 27 | Address: ip, 28 | Port: port, 29 | } 30 | return &s 31 | } 32 | 33 | func (d *DHCPServer) udpAddr() *net.UDPAddr { 34 | return &net.UDPAddr{ 35 | IP: d.Address, 36 | Port: d.Port, 37 | Zone: "", 38 | } 39 | } 40 | 41 | func (d *DHCPServer) String() string { 42 | if d.IsRC { 43 | return fmt.Sprintf("Hostname: %s, IP: %s, Port: %d (RC)", d.Hostname, d.Address, d.Port) 44 | } 45 | return fmt.Sprintf("Hostname: %s, IP: %s, Port: %d", d.Hostname, d.Address, d.Port) 46 | } 47 | -------------------------------------------------------------------------------- /lib/filesourcer.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "bufio" 12 | "net" 13 | "os" 14 | "path/filepath" 15 | "strconv" 16 | "sync" 17 | 18 | "github.com/fsnotify/fsnotify" 19 | "github.com/golang/glog" 20 | ) 21 | 22 | // FileSourcer holds various information about json the config files, list of 23 | // stable and rc servers, the fsnotify Watcher and stuff needed for 24 | // synchronization. 25 | type FileSourcer struct { 26 | stablePath string 27 | rcPath string 28 | version int 29 | watcher *fsnotify.Watcher 30 | lock sync.RWMutex 31 | stableServers []*DHCPServer 32 | rcServers []*DHCPServer 33 | } 34 | 35 | // NewFileSourcer returns a new FileSourcer, stablePath and rcPath are the paths 36 | // of the text files containing list of servers. If rcPath is empty it will be 37 | // ignored, stablePath must be not null, version is the protocol version and 38 | // should be either 4 or 6. 39 | func NewFileSourcer(stablePath, rcPath string, version int) (*FileSourcer, error) { 40 | watcher, err := fsnotify.NewWatcher() 41 | if err != nil { 42 | glog.Fatal(err) 43 | } 44 | err = watcher.Add(filepath.Dir(stablePath)) 45 | if err != nil { 46 | glog.Fatalf("Error watching stable: %s", err) 47 | } 48 | // RC is optional, only add to fsnotify and read if rcPath is present 49 | if len(rcPath) > 0 { 50 | err = watcher.Add(filepath.Dir(rcPath)) 51 | if err != nil { 52 | glog.Fatalf("Error watching rc: %s", err) 53 | } 54 | } 55 | sourcer, err := &FileSourcer{ 56 | stablePath: stablePath, 57 | rcPath: rcPath, 58 | version: version, 59 | watcher: watcher, 60 | }, nil 61 | sourcer.lock.Lock() 62 | sourcer.stableServers, err = sourcer.GetServersFromTier(stablePath) 63 | if err != nil { 64 | glog.Errorf("Failed to load stable servers: %s", err) 65 | } 66 | if len(rcPath) > 0 { 67 | sourcer.rcServers, err = sourcer.GetServersFromTier(rcPath) 68 | if err != nil { 69 | glog.Errorf("Failed to load RC servers: %s", err) 70 | } 71 | } 72 | sourcer.lock.Unlock() 73 | go sourcer.watchFsnotifyEvents() 74 | return sourcer, err 75 | } 76 | 77 | // GetServersFromTier returns a list of DHCPServer from a file 78 | func (fs *FileSourcer) GetServersFromTier(path string) ([]*DHCPServer, error) { 79 | inputFile, err := os.Open(path) 80 | if err != nil { 81 | return nil, err 82 | } 83 | defer inputFile.Close() 84 | scanner := bufio.NewScanner(inputFile) 85 | 86 | var servers []*DHCPServer 87 | for scanner.Scan() { 88 | var ( 89 | hostname string 90 | port int64 91 | ) 92 | line := scanner.Text() 93 | h, p, err := net.SplitHostPort(line) 94 | if err != nil { 95 | hostname = line 96 | if fs.version == 4 { 97 | port = 67 98 | } else { 99 | port = 547 100 | } 101 | } else { 102 | hostname = h 103 | var errPort error 104 | port, errPort = strconv.ParseInt(p, 10, 32) 105 | if errPort != nil { 106 | glog.Errorf("Can't convert port %s to int", p) 107 | continue 108 | } 109 | } 110 | ip := net.ParseIP(hostname) 111 | if ip == nil { 112 | ips, err := net.LookupHost(hostname) 113 | if err != nil { 114 | glog.Errorf("Can't resolve IPv4 for %s", hostname) 115 | continue 116 | } 117 | for i := range ips { 118 | addr := net.ParseIP(ips[i]) 119 | if addr != nil { 120 | if fs.version == 4 && addr.To4() != nil { 121 | ip = addr 122 | break 123 | } 124 | if fs.version == 6 && addr.To16() != nil { 125 | ip = addr 126 | break 127 | } 128 | } 129 | } 130 | } 131 | server := NewDHCPServer(hostname, ip, int(port)) 132 | servers = append(servers, server) 133 | } 134 | return servers, nil 135 | } 136 | 137 | func (fs *FileSourcer) watchFsnotifyEvents() { 138 | for { 139 | select { 140 | case ev := <-fs.watcher.Events: 141 | if ev.Op&fsnotify.Write != 0 { 142 | glog.Infof("Event: %s File changed, reloading host list", ev) 143 | fs.lock.Lock() 144 | var err error 145 | fs.stableServers, err = fs.GetServersFromTier(fs.stablePath) 146 | if err != nil { 147 | glog.Errorf("Failed to load stable servers: %s", err) 148 | } 149 | if len(fs.rcPath) > 0 { 150 | fs.rcServers, err = fs.GetServersFromTier(fs.rcPath) 151 | if err != nil { 152 | glog.Errorf("Failed to RC stable servers: %s", err) 153 | } 154 | } 155 | fs.lock.Unlock() 156 | } 157 | case err := <-fs.watcher.Errors: 158 | glog.Error("Error: ", err) 159 | } 160 | } 161 | } 162 | 163 | // GetStableServers returns a list of stable dhcp servers 164 | func (fs *FileSourcer) GetStableServers() ([]*DHCPServer, error) { 165 | return fs.stableServers, nil 166 | } 167 | 168 | // GetRCServers returns a list of rc dhcp servers 169 | func (fs *FileSourcer) GetRCServers() ([]*DHCPServer, error) { 170 | return fs.rcServers, nil 171 | } 172 | -------------------------------------------------------------------------------- /lib/handler.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "context" 12 | "errors" 13 | "fmt" 14 | "net" 15 | "runtime/debug" 16 | "time" 17 | 18 | "github.com/golang/glog" 19 | "github.com/insomniacslk/dhcp/dhcpv4" 20 | "github.com/insomniacslk/dhcp/dhcpv4/ztpv4" 21 | "github.com/insomniacslk/dhcp/dhcpv6" 22 | "github.com/insomniacslk/dhcp/dhcpv6/ztpv6" 23 | ) 24 | 25 | // List of possible errors. 26 | const ( 27 | ErrUnknown = "E_UNKNOWN" 28 | ErrPanic = "E_PANIC" 29 | ErrRead = "E_READ" 30 | ErrConnect = "E_CONN" 31 | ErrWrite = "E_WRITE" 32 | ErrGi0 = "E_GI_0" 33 | ErrParse = "E_PARSE" 34 | ErrNoServer = "E_NO_SERVER" 35 | ErrConnRate = "E_CONN_RATE" 36 | ) 37 | 38 | func (s *Server) handleConnection(ctx context.Context) { 39 | buffer := make([]byte, s.config.PacketBufSize) 40 | bytesRead, peer, err := s.conn.ReadFromUDP(buffer) 41 | if err != nil || bytesRead == 0 { 42 | msg := "error reading from %s: %v" 43 | glog.Errorf(msg, peer, err) 44 | s.logger.LogErr(time.Now(), nil, nil, peer, ErrRead, err) 45 | return 46 | } 47 | 48 | go func() { 49 | defer func() { 50 | if r := recover(); r != nil { 51 | glog.Errorf("Panicked handling v%d packet from %s: %s", s.config.Version, peer, r) 52 | glog.Errorf("Offending packet: %x", buffer[:bytesRead]) 53 | err, _ := r.(error) 54 | s.logger.LogErr(time.Now(), nil, nil, peer, ErrPanic, err) 55 | glog.Errorf("%s: %s", r, debug.Stack()) 56 | } 57 | }() 58 | 59 | if s.config.Version == 4 { 60 | s.handleRawPacketV4(ctx, buffer[:bytesRead], peer) 61 | } else if s.config.Version == 6 { 62 | s.handleRawPacketV6(ctx, buffer[:bytesRead], peer) 63 | } 64 | }() 65 | } 66 | 67 | func selectDestinationServer(config *Config, message *DHCPMessage) (*DHCPServer, error) { 68 | server, err := handleOverride(config, message) 69 | if err != nil { 70 | glog.Errorf("Error handling override, drop due to: %s", err) 71 | return nil, err 72 | } 73 | if server == nil { 74 | server, err = config.Algorithm.SelectRatioBasedDhcpServer(message) 75 | } 76 | return server, err 77 | } 78 | 79 | func handleOverride(config *Config, message *DHCPMessage) (*DHCPServer, error) { 80 | if override, ok := config.Overrides[message.Mac.String()]; ok { 81 | // Checking if override is expired. If so, ignore it. Expiration field should 82 | // be a timestamp in the following format "2006/01/02 15:04 -0700". 83 | // For example, a timestamp in UTC would look as follows: "2017/05/06 14:00 +0000". 84 | var err error 85 | var expiration time.Time 86 | if override.Expiration != "" { 87 | expiration, err = time.Parse("2006/01/02 15:04 -0700", override.Expiration) 88 | if err != nil { 89 | glog.Errorf("Could not parse override expiration for MAC %s: %s", message.Mac.String(), err.Error()) 90 | return nil, nil 91 | } 92 | if time.Now().After(expiration) { 93 | glog.Errorf("Override rule for MAC %s expired on %s, ignoring", message.Mac.String(), expiration.Local()) 94 | return nil, nil 95 | } 96 | } 97 | if override.Expiration == "" { 98 | glog.Infof("Found override rule for %s without expiration", message.Mac.String()) 99 | } else { 100 | glog.Infof("Found override rule for %s, it will expire on %s", message.Mac.String(), expiration.Local()) 101 | } 102 | 103 | var server *DHCPServer 104 | if len(override.Host) > 0 { 105 | server, err = handleHostOverride(config, override.Host) 106 | } else if len(override.Tier) > 0 { 107 | server, err = handleTierOverride(config, override.Tier, message) 108 | } 109 | if err != nil { 110 | return nil, err 111 | } 112 | if server != nil { 113 | return server, nil 114 | } 115 | glog.Infof("Override didn't have host or tier, this shouldn't happen, proceeding with normal server selection") 116 | } 117 | return nil, nil 118 | } 119 | 120 | func handleHostOverride(config *Config, host string) (*DHCPServer, error) { 121 | addr := net.ParseIP(host) 122 | if addr == nil { 123 | return nil, fmt.Errorf("Failed to get IP for overridden host %s", host) 124 | } 125 | port := 67 126 | if config.Version == 6 { 127 | port = 547 128 | } 129 | server := NewDHCPServer(host, addr, port) 130 | return server, nil 131 | } 132 | 133 | func handleTierOverride(config *Config, tier string, message *DHCPMessage) (*DHCPServer, error) { 134 | servers, err := config.HostSourcer.GetServersFromTier(tier) 135 | if err != nil { 136 | return nil, fmt.Errorf("Failed to get servers from tier: %s", err) 137 | } 138 | if len(servers) == 0 { 139 | return nil, fmt.Errorf("Sourcer returned no servers") 140 | } 141 | // pick server according to the configured algorithm 142 | server, err := config.Algorithm.SelectServerFromList(servers, message) 143 | if err != nil { 144 | return nil, fmt.Errorf("Failed to select server: %s", err) 145 | } 146 | return server, nil 147 | } 148 | 149 | func (s *Server) sendToServer(start time.Time, server *DHCPServer, packet []byte, peer *net.UDPAddr) error { 150 | 151 | // Check for connection rate 152 | ok, err := s.throttle.OK(server.Address.String()) 153 | if !ok { 154 | glog.Errorf("Error writing to server %s, drop due to throttling", server.Hostname) 155 | s.logger.LogErr(time.Now(), server, packet, peer, ErrConnRate, err) 156 | return err 157 | } 158 | 159 | _, err = s.conn.WriteTo(packet, server.udpAddr()) 160 | if err != nil { 161 | glog.Errorf("Error writing to server %s, drop due to %s", server.Hostname, err) 162 | s.logger.LogErr(start, server, packet, peer, ErrWrite, err) 163 | return err 164 | } 165 | 166 | s.logger.LogSuccess(start, server, packet, peer) 167 | 168 | return nil 169 | } 170 | 171 | func (s *Server) handleRawPacketV4(ctx context.Context, buffer []byte, peer *net.UDPAddr) { 172 | // runs in a separate go routine 173 | start := time.Now() 174 | var message DHCPMessage 175 | packet, err := dhcpv4.FromBytes(buffer) 176 | if err != nil { 177 | glog.Errorf("Error encoding DHCPv4 packet: %s", err) 178 | s.logger.LogErr(start, nil, nil, peer, ErrParse, err) 179 | return 180 | } 181 | 182 | if s.server { 183 | s.handleV4Server(ctx, start, packet, peer) 184 | return 185 | } 186 | 187 | message.XID = packet.TransactionID[:] 188 | message.Peer = peer 189 | message.ClientID = packet.ClientHWAddr 190 | message.Mac = packet.ClientHWAddr 191 | if vd, err := ztpv4.ParseVendorData(packet); err != nil { 192 | glog.V(2).Infof("error parsing vendor data: %s", err) 193 | } else { 194 | message.Serial = vd.Serial 195 | } 196 | 197 | packet.HopCount++ 198 | 199 | server, err := selectDestinationServer(s.config, &message) 200 | if err != nil { 201 | glog.Errorf("%s, Drop due to %s", packet.Summary(), err) 202 | s.logger.LogErr(start, nil, packet.ToBytes(), peer, ErrNoServer, err) 203 | return 204 | } 205 | 206 | s.sendToServer(start, server, packet.ToBytes(), peer) 207 | } 208 | 209 | func (s *Server) handleV4Server(ctx context.Context, start time.Time, packet *dhcpv4.DHCPv4, peer *net.UDPAddr) { 210 | reply, err := s.config.Handler.ServeDHCPv4(ctx, packet) 211 | s.logger.LogSuccess(start, nil, packet.ToBytes(), peer) 212 | if err != nil { 213 | glog.Errorf("Error creating reply %s", err) 214 | s.logger.LogErr(start, nil, packet.ToBytes(), peer, fmt.Sprintf("%T", err), err) 215 | return 216 | } 217 | addr := &net.UDPAddr{ 218 | IP: packet.GatewayIPAddr, 219 | Port: dhcpv4.ServerPort, 220 | } 221 | s.conn.WriteTo(reply.ToBytes(), addr) 222 | s.logger.LogSuccess(start, nil, reply.ToBytes(), peer) 223 | } 224 | 225 | func (s *Server) handleRawPacketV6(ctx context.Context, buffer []byte, peer *net.UDPAddr) { 226 | // runs in a separate go routine 227 | start := time.Now() 228 | packet, err := dhcpv6.FromBytes(buffer) 229 | if err != nil { 230 | glog.Errorf("Error encoding DHCPv6 packet: %s", err) 231 | s.logger.LogErr(start, nil, nil, peer, ErrParse, err) 232 | return 233 | } 234 | 235 | if s.server { 236 | s.handleV6Server(ctx, start, packet, peer) 237 | return 238 | } 239 | 240 | if packet.Type() == dhcpv6.MessageTypeRelayReply { 241 | s.handleV6RelayRepl(start, packet, peer) 242 | return 243 | } 244 | 245 | var message DHCPMessage 246 | 247 | msg, err := packet.GetInnerMessage() 248 | if err != nil { 249 | glog.Errorf("Error getting inner message: %s", err) 250 | s.logger.LogErr(start, nil, packet.ToBytes(), peer, ErrParse, err) 251 | return 252 | } 253 | message.XID = msg.TransactionID[:] 254 | message.Peer = peer 255 | 256 | duid := msg.Options.ClientID() 257 | if duid == nil { 258 | errMsg := errors.New("failed to extract Client ID") 259 | glog.Errorf("%v", errMsg) 260 | s.logger.LogErr(start, nil, packet.ToBytes(), peer, ErrParse, errMsg) 261 | return 262 | } 263 | message.ClientID = duid.ToBytes() 264 | mac, err := dhcpv6.ExtractMAC(packet) 265 | if err != nil { 266 | glog.Errorf("Failed to extract MAC, drop due to %s", err) 267 | s.logger.LogErr(start, nil, packet.ToBytes(), peer, ErrParse, err) 268 | return 269 | } 270 | message.Mac = mac 271 | if vendorData, err := ztpv6.ParseVendorData(msg); err != nil { 272 | glog.V(2).Infof("Failed to extract vendor data: %s", err) 273 | } else { 274 | message.Serial = vendorData.Serial 275 | } 276 | 277 | server, err := selectDestinationServer(s.config, &message) 278 | if err != nil { 279 | glog.Errorf("%s, Drop due to %s", packet.Summary(), err) 280 | s.logger.LogErr(start, nil, packet.ToBytes(), peer, ErrNoServer, err) 281 | return 282 | } 283 | 284 | relayMsg, _ := dhcpv6.EncapsulateRelay(packet, dhcpv6.MessageTypeRelayForward, net.IPv6zero, peer.IP) 285 | s.sendToServer(start, server, relayMsg.ToBytes(), peer) 286 | } 287 | 288 | func (s *Server) handleV6RelayRepl(start time.Time, packet dhcpv6.DHCPv6, peer *net.UDPAddr) { 289 | // when we get a relay-reply, we need to unwind the message, removing the top 290 | // relay-reply info and passing on the inner part of the message 291 | msg, err := dhcpv6.DecapsulateRelay(packet) 292 | if err != nil { 293 | glog.Errorf("Failed to decapsulate packet, drop due to %s", err) 294 | s.logger.LogErr(start, nil, packet.ToBytes(), peer, ErrParse, err) 295 | return 296 | } 297 | peerAddr := packet.(*dhcpv6.RelayMessage).PeerAddr 298 | // send the packet to the peer addr 299 | addr := &net.UDPAddr{ 300 | IP: peerAddr, 301 | Port: dhcpv6.DefaultServerPort, 302 | Zone: "", 303 | } 304 | conn, err := net.DialUDP("udp", s.config.ReplyAddr, addr) 305 | if err != nil { 306 | glog.Errorf("Error creating udp connection %s", err) 307 | s.logger.LogErr(start, nil, packet.ToBytes(), peer, ErrConnect, err) 308 | return 309 | } 310 | conn.Write(msg.ToBytes()) 311 | s.logger.LogSuccess(start, nil, packet.ToBytes(), peer) 312 | conn.Close() 313 | } 314 | 315 | func (s *Server) handleV6Server(ctx context.Context, start time.Time, packet dhcpv6.DHCPv6, peer *net.UDPAddr) { 316 | reply, err := s.config.Handler.ServeDHCPv6(ctx, packet) 317 | s.logger.LogSuccess(start, nil, packet.ToBytes(), peer) 318 | if err != nil { 319 | glog.Errorf("Error creating reply %s", err) 320 | s.logger.LogErr(start, nil, packet.ToBytes(), peer, fmt.Sprintf("%T", err), err) 321 | return 322 | } 323 | addr := &net.UDPAddr{ 324 | IP: peer.IP, 325 | Port: dhcpv6.DefaultServerPort, 326 | } 327 | s.conn.WriteTo(reply.ToBytes(), addr) 328 | s.logger.LogSuccess(start, nil, reply.ToBytes(), peer) 329 | } 330 | -------------------------------------------------------------------------------- /lib/interface.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "context" 12 | "net" 13 | 14 | "github.com/insomniacslk/dhcp/dhcpv4" 15 | "github.com/insomniacslk/dhcp/dhcpv6" 16 | ) 17 | 18 | // DHCPMessage represents coordinates of a dhcp message. 19 | type DHCPMessage struct { 20 | XID []byte 21 | Peer *net.UDPAddr 22 | ClientID []byte 23 | Mac net.HardwareAddr 24 | Serial string 25 | } 26 | 27 | // DHCPBalancingAlgorithm defines an interface for load balancing algorithms. 28 | // Users can implement their own and add them to config.go (in the 29 | // configSpec.algorithm method) 30 | type DHCPBalancingAlgorithm interface { 31 | SelectServerFromList(list []*DHCPServer, message *DHCPMessage) (*DHCPServer, error) 32 | SelectRatioBasedDhcpServer(message *DHCPMessage) (*DHCPServer, error) 33 | UpdateStableServerList(list []*DHCPServer) error 34 | UpdateRCServerList(list []*DHCPServer) error 35 | SetRCRatio(ratio uint32) 36 | // An unique name for the algorithm, this string can be used in the 37 | // configuration file, in the section where the algorithm is selecetd. 38 | Name() string 39 | } 40 | 41 | // DHCPServerSourcer is an interface used to fetch stable, rc and servers from 42 | // a "tier" (group of servers). 43 | type DHCPServerSourcer interface { 44 | GetStableServers() ([]*DHCPServer, error) 45 | GetRCServers() ([]*DHCPServer, error) 46 | GetServersFromTier(tier string) ([]*DHCPServer, error) 47 | } 48 | 49 | // Handler is an interface used while serving DHCP requests. 50 | type Handler interface { 51 | ServeDHCPv4(ctx context.Context, packet *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, error) 52 | ServeDHCPv6(ctx context.Context, packet dhcpv6.DHCPv6) (dhcpv6.DHCPv6, error) 53 | } 54 | -------------------------------------------------------------------------------- /lib/log.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "net" 12 | "time" 13 | 14 | "github.com/golang/glog" 15 | ) 16 | 17 | // LogMessage holds the info of a log line. 18 | type LogMessage struct { 19 | Version int 20 | Packet []byte 21 | Peer *net.UDPAddr 22 | Server string 23 | ServerIsRC bool 24 | Latency time.Duration 25 | Success bool 26 | ErrorName string 27 | ErrorDetails error 28 | } 29 | 30 | // PersonalizedLogger is an interface used to log a LogMessage using your own 31 | // logic. It will be used in loggerHelperImpl. 32 | type PersonalizedLogger interface { 33 | Log(msg LogMessage) error 34 | } 35 | 36 | // loggerHelper is the implementation of the above interface. 37 | type loggerHelper struct { 38 | personalizedLogger PersonalizedLogger 39 | version int 40 | } 41 | 42 | func (h *loggerHelper) LogErr(start time.Time, server *DHCPServer, packet []byte, peer *net.UDPAddr, errName string, err error) { 43 | if h.personalizedLogger != nil { 44 | hostname := "" 45 | isRC := false 46 | if server != nil { 47 | hostname = server.Hostname 48 | isRC = server.IsRC 49 | } 50 | msg := LogMessage{ 51 | Version: h.version, 52 | Packet: packet, 53 | Peer: peer, 54 | Server: hostname, 55 | ServerIsRC: isRC, 56 | Latency: time.Since(start), 57 | Success: false, 58 | ErrorName: errName, 59 | ErrorDetails: err, 60 | } 61 | err := h.personalizedLogger.Log(msg) 62 | if err != nil { 63 | glog.Errorf("Failed to log error: %s", err) 64 | } 65 | } 66 | } 67 | 68 | func (h *loggerHelper) LogSuccess(start time.Time, server *DHCPServer, packet []byte, peer *net.UDPAddr) { 69 | if h.personalizedLogger != nil { 70 | hostname := "" 71 | isRC := false 72 | if server != nil { 73 | hostname = server.Hostname 74 | isRC = server.IsRC 75 | } 76 | msg := LogMessage{ 77 | Version: h.version, 78 | Packet: packet, 79 | Peer: peer, 80 | Server: hostname, 81 | ServerIsRC: isRC, 82 | Latency: time.Since(start), 83 | Success: true, 84 | } 85 | err := h.personalizedLogger.Log(msg) 86 | if err != nil { 87 | glog.Errorf("Failed to log success: %s", err) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/modulo.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "errors" 12 | "github.com/golang/glog" 13 | "hash/fnv" 14 | "sync" 15 | "sync/atomic" 16 | ) 17 | 18 | type modulo struct { 19 | lock sync.RWMutex 20 | stable []*DHCPServer 21 | rc []*DHCPServer 22 | rcRatio uint32 23 | } 24 | 25 | func (m *modulo) Name() string { 26 | return "xid" 27 | } 28 | 29 | func (m *modulo) getHash(token []byte) uint32 { 30 | hasher := fnv.New32a() 31 | hasher.Write(token) 32 | hash := hasher.Sum32() 33 | return hash 34 | } 35 | 36 | func (m *modulo) SetRCRatio(ratio uint32) { 37 | atomic.StoreUint32(&m.rcRatio, ratio) 38 | } 39 | 40 | func (m *modulo) SelectServerFromList(list []*DHCPServer, message *DHCPMessage) (*DHCPServer, error) { 41 | hash := m.getHash(message.ClientID) 42 | if len(list) == 0 { 43 | return nil, errors.New("Server list is empty") 44 | } 45 | return list[hash%uint32(len(list))], nil 46 | } 47 | 48 | func (m *modulo) SelectRatioBasedDhcpServer(message *DHCPMessage) (*DHCPServer, error) { 49 | m.lock.RLock() 50 | defer m.lock.RUnlock() 51 | 52 | hash := m.getHash(message.ClientID) 53 | 54 | // convert to a number 0-100 and then see if it should be RC 55 | if hash%100 < m.rcRatio { 56 | return m.SelectServerFromList(m.rc, message) 57 | } 58 | // otherwise go to stable 59 | return m.SelectServerFromList(m.stable, message) 60 | } 61 | 62 | func (m *modulo) UpdateServerList(name string, list []*DHCPServer, ptr *[]*DHCPServer) error { 63 | m.lock.Lock() 64 | defer m.lock.Unlock() 65 | 66 | *ptr = list 67 | glog.Infof("List of available %s servers:", name) 68 | for _, server := range *ptr { 69 | glog.Infof("%s", server) 70 | } 71 | return nil 72 | } 73 | 74 | func (m *modulo) UpdateStableServerList(list []*DHCPServer) error { 75 | return m.UpdateServerList("stable", list, &m.stable) 76 | } 77 | 78 | func (m *modulo) UpdateRCServerList(list []*DHCPServer) error { 79 | return m.UpdateServerList("rc", list, &m.rc) 80 | } 81 | -------------------------------------------------------------------------------- /lib/modulo_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "testing" 12 | ) 13 | 14 | func Test_Empty(t *testing.T) { 15 | subject := new(modulo) 16 | _, err := subject.SelectRatioBasedDhcpServer(&DHCPMessage{ 17 | ClientID: []byte{0}, 18 | }) 19 | if err == nil { 20 | t.Fatalf("Should throw an error if server list is empty") 21 | } 22 | } 23 | 24 | func Test_Hash(t *testing.T) { 25 | // these are randomly generated "client ids" that are known to result in 26 | // FNV-1a 32 bit hashes 0-4 after %4 27 | tests := [][]byte{ 28 | {0xf6, 0x85, 0x63, 0x3, 0x11, 0x80, 0x72, 0x97, 0x23, 0xa1}, 29 | {0x8c, 0x41, 0x34, 0xe1, 0x9c, 0xd, 0xfc, 0xe5, 0x41, 0x4b}, 30 | {0x54, 0xc9, 0xeb, 0x57, 0xa, 0x57, 0x14, 0x43, 0x2b, 0x19}, 31 | {0x54, 0xc5, 0x89, 0x66, 0xb2, 0xdc, 0x39, 0xf7, 0x8f, 0xa5}, 32 | } 33 | subject := new(modulo) 34 | servers := make([]*DHCPServer, 4) 35 | for i := 0; i < 4; i++ { 36 | servers[i] = &DHCPServer{ 37 | Port: i, //use port to tell if we picked the right one 38 | } 39 | } 40 | subject.UpdateStableServerList(servers) 41 | for i, v := range tests { 42 | msg := DHCPMessage{ 43 | ClientID: v, 44 | } 45 | server, err := subject.SelectRatioBasedDhcpServer(&msg) 46 | if err != nil { 47 | t.Fatalf("Unexpected error selecting server: %s", err) 48 | } 49 | if server.Port != i { 50 | t.Fatalf("Chose wrong server for %x, was expecting %d, got %d", 51 | v, i, server.Port) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/rr.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "errors" 12 | "hash/fnv" 13 | "sync" 14 | "sync/atomic" 15 | 16 | "github.com/golang/glog" 17 | ) 18 | 19 | type roundRobin struct { 20 | lock sync.RWMutex 21 | stable []*DHCPServer 22 | rc []*DHCPServer 23 | rcRatio uint32 24 | iterStable int 25 | iterRC int 26 | iterList int // iterator used by SelectServerFromList, can be used in stable/rc or passing list manually 27 | } 28 | 29 | func (rr *roundRobin) Name() string { 30 | return "rr" 31 | } 32 | 33 | func (rr *roundRobin) getHash(token []byte) uint32 { 34 | hasher := fnv.New32a() 35 | hasher.Write(token) 36 | hash := hasher.Sum32() 37 | return hash 38 | } 39 | 40 | func (rr *roundRobin) SetRCRatio(ratio uint32) { 41 | atomic.StoreUint32(&rr.rcRatio, ratio) 42 | } 43 | 44 | func (rr *roundRobin) SelectServerFromList(list []*DHCPServer, message *DHCPMessage) (*DHCPServer, error) { 45 | rr.lock.RLock() 46 | defer rr.lock.RUnlock() 47 | 48 | if len(list) == 0 { 49 | return nil, errors.New("Server list is empty") 50 | } 51 | // no guarantee that lists are the same size, so modulo before incrementing 52 | rr.iterList = rr.iterList % len(list) 53 | server := list[rr.iterList] 54 | rr.iterList++ 55 | return server, nil 56 | } 57 | 58 | func (rr *roundRobin) SelectRatioBasedDhcpServer(message *DHCPMessage) (server *DHCPServer, err error) { 59 | // hash the clientid to see if it should be RC/Stable 60 | hash := rr.getHash(message.ClientID) 61 | 62 | rr.lock.Lock() 63 | 64 | if hash%100 < rr.rcRatio { 65 | rr.iterList = rr.iterRC 66 | rr.iterRC++ 67 | rr.lock.Unlock() 68 | return rr.SelectServerFromList(rr.rc, message) 69 | } 70 | //otherwise go stable 71 | rr.iterList = rr.iterStable 72 | rr.iterStable++ 73 | rr.lock.Unlock() 74 | return rr.SelectServerFromList(rr.stable, message) 75 | } 76 | 77 | func (rr *roundRobin) UpdateServerList(name string, list []*DHCPServer, ptr *[]*DHCPServer) error { 78 | rr.lock.Lock() 79 | defer rr.lock.Unlock() 80 | 81 | *ptr = list 82 | rr.iterStable = 0 83 | rr.iterRC = 0 84 | glog.Infof("List of available %s servers:", name) 85 | for _, server := range *ptr { 86 | glog.Infof("%s", server) 87 | } 88 | return nil 89 | } 90 | 91 | func (rr *roundRobin) UpdateStableServerList(list []*DHCPServer) error { 92 | return rr.UpdateServerList("stable", list, &rr.stable) 93 | } 94 | 95 | func (rr *roundRobin) UpdateRCServerList(list []*DHCPServer) error { 96 | return rr.UpdateServerList("rc", list, &rr.rc) 97 | } 98 | -------------------------------------------------------------------------------- /lib/rr_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "testing" 12 | ) 13 | 14 | func TestRREmpty(t *testing.T) { 15 | subject := new(roundRobin) 16 | _, err := subject.SelectRatioBasedDhcpServer(&DHCPMessage{ 17 | ClientID: []byte{0}, 18 | }) 19 | if err == nil { 20 | t.Fatalf("Should throw an error if server list is empty") 21 | } 22 | } 23 | 24 | func TestRRBalance(t *testing.T) { 25 | subject := new(roundRobin) 26 | servers := make([]*DHCPServer, 4) 27 | for i := 0; i < 4; i++ { 28 | servers[i] = &DHCPServer{ 29 | Port: i, 30 | } 31 | } 32 | subject.UpdateStableServerList(servers) 33 | msg := DHCPMessage{ 34 | ClientID: []byte{0}, 35 | } 36 | for i := 0; i < 4; i++ { 37 | server, err := subject.SelectRatioBasedDhcpServer(&msg) 38 | if err != nil { 39 | t.Fatalf("Unexpected error selecting server: %s", err) 40 | } 41 | if server.Port != i { 42 | t.Fatalf("Chose wrong server %d, expected %d", server.Port, i) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/server.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "context" 12 | "net" 13 | "sync/atomic" 14 | "unsafe" 15 | 16 | "github.com/golang/glog" 17 | ) 18 | 19 | // UDP acceptor 20 | type Server struct { 21 | server bool 22 | conn *net.UDPConn 23 | logger *loggerHelper 24 | config *Config 25 | stableServers []*DHCPServer 26 | rcServers []*DHCPServer 27 | throttle *Throttle 28 | } 29 | 30 | // returns a pointer to the current config struct, so that if it does get changed while being used, 31 | // it shouldn't affect the caller and this copy struct should be GC'ed when it falls out of scope 32 | func (s *Server) GetConfig() *Config { 33 | return s.config 34 | } 35 | 36 | // ListenAndServe starts the server 37 | func (s *Server) ListenAndServe(ctx context.Context) error { 38 | if !s.server { 39 | s.startUpdatingServerList() 40 | } 41 | 42 | glog.Infof("Started server, processing DHCP requests...") 43 | 44 | for { 45 | s.handleConnection(ctx) 46 | } 47 | } 48 | 49 | // SetConfig updates the server config 50 | func (s *Server) SetConfig(config *Config) { 51 | glog.Infof("Updating server config") 52 | // update server list because Algorithm instance was recreated 53 | config.Algorithm.UpdateStableServerList(s.stableServers) 54 | config.Algorithm.UpdateRCServerList(s.rcServers) 55 | atomic.SwapPointer((*unsafe.Pointer)(unsafe.Pointer(&s.config)), unsafe.Pointer(config)) 56 | // update the throttle rate 57 | s.throttle.setRate(config.Rate) 58 | glog.Infof("Updated server config") 59 | } 60 | 61 | // HasServers checks if the list of backend servers is not empty 62 | func (s *Server) HasServers() bool { 63 | return len(s.stableServers) > 0 || len(s.rcServers) > 0 64 | } 65 | 66 | // NewServer initialized a Server before returning it. 67 | func NewServer(config *Config, serverMode bool, personalizedLogger PersonalizedLogger) (*Server, error) { 68 | conn, err := net.ListenUDP("udp", config.Addr) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | // setup logger 74 | var loggerHelper = &loggerHelper{ 75 | version: config.Version, 76 | personalizedLogger: personalizedLogger, 77 | } 78 | 79 | server := &Server{ 80 | server: serverMode, 81 | conn: conn, 82 | logger: loggerHelper, 83 | config: config, 84 | } 85 | 86 | glog.Infof("Setting up throttle: Cache Size: %d - Cache Rate: %d - Request Rate: %d", 87 | config.CacheSize, config.CacheRate, config.Rate) 88 | throttle, err := NewThrottle(config.CacheSize, config.CacheRate, config.Rate) 89 | if err != nil { 90 | return nil, err 91 | } 92 | server.throttle = throttle 93 | 94 | return server, nil 95 | } 96 | -------------------------------------------------------------------------------- /lib/throttle.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "fmt" 12 | "sync" 13 | 14 | "github.com/golang/glog" 15 | lru "github.com/hashicorp/golang-lru/v2" 16 | "golang.org/x/time/rate" 17 | ) 18 | 19 | // An LRU cache implementation of Throttle. 20 | // 21 | // We keep track of request rates per client in an LRU cache to 22 | // keep memory usage under control against malicious requests. Each 23 | // value in the cache is a rate.Limiter struct which is an implementation 24 | // of Taken Bucket algorithm. 25 | // 26 | // Adding new items to the cache is also limited to control cache 27 | // invalidation rate. 28 | type Throttle struct { 29 | mu sync.Mutex 30 | lru *lru.Cache[string, *rate.Limiter] 31 | maxRatePerItem int 32 | cacheLimiter *rate.Limiter 33 | cacheRate int 34 | } 35 | 36 | // Returns true if the rate is below maximum for the given key 37 | func (c *Throttle) OK(key string) (bool, error) { 38 | if c.maxRatePerItem <= 0 { 39 | return true, nil 40 | } 41 | 42 | c.mu.Lock() 43 | defer c.mu.Unlock() 44 | 45 | // If the limiter is not in the cache for the given key 46 | // check for the cache limiter. If it is below the maximum, 47 | // then create a limiter, add it to the cache and allocate a bucket. 48 | limiter, ok := c.lru.Get(key) 49 | if !ok { 50 | if c.cacheLimiter.Allow() { 51 | limiter := rate.NewLimiter(rate.Limit(c.maxRatePerItem), c.maxRatePerItem) 52 | c.lru.Add(key, limiter) 53 | 54 | return limiter.Allow(), nil 55 | } 56 | 57 | err := fmt.Errorf("Cache invalidation is too fast (max: %d item/sec) - throttling", c.cacheRate) 58 | return false, err 59 | } 60 | 61 | // So the limiter object is in the cache. Try to allocate a bucket. 62 | if !limiter.Allow() { 63 | err := fmt.Errorf("Request rate is too high for %v (max: %d req/sec) - throttling", key, c.maxRatePerItem) 64 | return false, err 65 | } 66 | 67 | return true, nil 68 | } 69 | 70 | func (c *Throttle) len() int { 71 | return c.lru.Len() 72 | } 73 | 74 | func (c *Throttle) setRate(MaxRatePerItem int) { 75 | c.mu.Lock() 76 | defer c.mu.Unlock() 77 | c.maxRatePerItem = MaxRatePerItem 78 | } 79 | 80 | // NewThrottle returns a Throttle struct 81 | // 82 | // capacity: 83 | // Maximum capacity of the LRU cache 84 | // 85 | // cacheRate (per second): 86 | // Maximum allowed rate for adding new items to the cache. By that way it 87 | // prevents the cache invalidation to happen too soon for the existing rate 88 | // items in the cache. Cache rate will be infinite for 0 or negative values. 89 | // 90 | // maxRatePerItem (per second): 91 | // Maximum allowed requests rate for each key in the cache. Throttling will 92 | // be disabled for 0 or negative values. No cache will be created in that case. 93 | func NewThrottle(capacity int, cacheRate int, maxRatePerItem int) (*Throttle, error) { 94 | if maxRatePerItem <= 0 { 95 | glog.Info("No throttling will be done") 96 | } 97 | 98 | cache, err := lru.New[string, *rate.Limiter](capacity) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | // Keep track of the item creation rate. 104 | var cacheLimiter *rate.Limiter 105 | if cacheRate <= 0 { 106 | glog.Info("No cache rate limiting will be done") 107 | cacheLimiter = rate.NewLimiter(rate.Inf, 1) // bucket size is ignored 108 | } else { 109 | cacheLimiter = rate.NewLimiter(rate.Limit(cacheRate), cacheRate) 110 | } 111 | 112 | throttle := &Throttle{ 113 | lru: cache, 114 | maxRatePerItem: maxRatePerItem, 115 | cacheLimiter: cacheLimiter, 116 | cacheRate: cacheRate, 117 | } 118 | 119 | return throttle, nil 120 | } 121 | -------------------------------------------------------------------------------- /lib/throttle_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "fmt" 12 | "math" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | func Test_ThrottleArgs(t *testing.T) { 18 | // Test invalid cache size 19 | _, err := NewThrottle(-1, 128, 128) 20 | if err == nil { 21 | t.Fatalf("Should return error on negative cache size") 22 | } 23 | } 24 | 25 | func Test_ThrottleOff(t *testing.T) { 26 | // Test non throttling option 27 | throttle, err := NewThrottle(128, 64, -1) 28 | if err != nil { 29 | t.Fatalf("Error creating a throttle: %s", err) 30 | } 31 | 32 | for i := 0; i < 1000; i++ { 33 | // Test one key multiple requests 34 | if ok, err := throttle.OK("my_key"); !ok { 35 | t.Fatalf("Throttling disabled, shouldn't throttle requests: %s", err) 36 | } 37 | 38 | // Test multiple keys 39 | key := fmt.Sprintf("my_key_%d", i) 40 | if ok, err := throttle.OK(key); !ok { 41 | t.Fatalf("Throttling disabled, shouldn't throttle new items: %s", err) 42 | } 43 | } 44 | } 45 | 46 | func Test_ThrottleCacheRateOff(t *testing.T) { 47 | throttle, err := NewThrottle(128, -1, 64) 48 | if err != nil { 49 | t.Fatalf("Error creating a throttle: %s", err) 50 | } 51 | 52 | for i := 0; i < 1000; i++ { 53 | // Test multiple keys 54 | key := fmt.Sprintf("my_key_%d", i) 55 | if ok, err := throttle.OK(key); !ok { 56 | t.Fatalf("Cache rate limiting is disabled, shouldn't throttle new items: %s", err) 57 | } 58 | } 59 | } 60 | 61 | func Test_ThrottleLRU(t *testing.T) { 62 | const cacheSize = 128 63 | const cacheRate = 128 // per sec 64 | const connRate = 1 // per sec 65 | 66 | sleepTime := time.Duration(math.Ceil((1000. / cacheRate) / 2)) 67 | loopCount := cacheSize * 2 68 | 69 | throttle, err := NewThrottle(cacheSize, cacheRate, connRate) 70 | if err != nil { 71 | t.Fatalf("Error creating a throttle: %v", err) 72 | } 73 | for i := 0; i < loopCount; i++ { 74 | key := fmt.Sprintf("my_key_%d", i) 75 | 76 | throttle.OK(key) 77 | 78 | time.Sleep(sleepTime * time.Millisecond) 79 | } 80 | if throttle.len() != cacheSize { 81 | t.Fatalf("Throttle LRU size is wrong - expected value: %d", cacheSize) 82 | } 83 | } 84 | 85 | func Test_ThrottleSingleConnection(t *testing.T) { 86 | const cacheSize = 1 87 | const cacheRate = 1 // per sec 88 | const connRate = 64 // per sec 89 | const key = "test_key" 90 | 91 | sleepDuration := time.Duration(math.Ceil(1000. / connRate)) 92 | loopCount := connRate 93 | 94 | throttle, err := NewThrottle(cacheSize, cacheRate, connRate) 95 | if err != nil { 96 | t.Fatalf("Error creating a throttle: %v", err) 97 | } 98 | for i := 0; i < loopCount; i++ { 99 | if ok, err := throttle.OK(key); !ok { 100 | t.Fatalf("Throttling failed for single connection: %s", err) 101 | } 102 | 103 | time.Sleep(sleepDuration * time.Millisecond) 104 | } 105 | } 106 | 107 | func Test_ThrottleSingleConnectionFail(t *testing.T) { 108 | const cacheSize = 1 109 | const cacheRate = 1 // per sec 110 | const connRate = 64 // per sec 111 | const key = "test_key" 112 | 113 | sleepTime := time.Duration((1000 / connRate) / 3) 114 | loopCount := connRate * 3 115 | 116 | throttle, err := NewThrottle(cacheSize, cacheRate, connRate) 117 | if err != nil { 118 | t.Fatalf("Error creating a throttle: %v", err) 119 | } 120 | for i := 0; i < loopCount; i++ { 121 | if ok, _ := throttle.OK(key); !ok { 122 | return 123 | } 124 | 125 | time.Sleep(sleepTime * time.Millisecond) 126 | } 127 | 128 | t.Fatalf("Throttling didn't work for single connection!") 129 | } 130 | 131 | func Test_ThrottleCacheRate(t *testing.T) { 132 | const cacheSize = 1024 133 | const cacheRate = 64 134 | const connRate = 1 135 | 136 | sleepTime := time.Duration(math.Ceil(1000. / cacheRate)) 137 | loopCount := cacheRate 138 | 139 | throttle, err := NewThrottle(cacheSize, cacheRate, connRate) 140 | if err != nil { 141 | t.Fatalf("Error creating a throttle: %v", err) 142 | } 143 | for i := 0; i < loopCount; i++ { 144 | key := fmt.Sprintf("my_key_%d", i) 145 | 146 | if ok, err := throttle.OK(key); !ok { 147 | t.Fatalf("Throttling failed for cache rate limiting: %s", err) 148 | } 149 | 150 | time.Sleep(sleepTime * time.Millisecond) 151 | } 152 | } 153 | 154 | func Test_ThrottleCacheRateFail(t *testing.T) { 155 | const cacheSize = 1024 156 | const cacheRate = 64 157 | const connRate = 1 158 | 159 | sleepTime := time.Duration((1000 / cacheRate) / 3) 160 | loopCount := cacheRate * 3 161 | 162 | throttle, err := NewThrottle(cacheSize, cacheRate, connRate) 163 | if err != nil { 164 | t.Fatalf("Error creating a throttle: %v", err) 165 | } 166 | for i := 0; i < loopCount; i++ { 167 | key := fmt.Sprintf("my_key_%d", i) 168 | 169 | if ok, _ := throttle.OK(key); !ok { 170 | return 171 | } 172 | 173 | time.Sleep(sleepTime * time.Millisecond) 174 | } 175 | 176 | t.Fatalf("Throttling didn't work for cache rate limiting") 177 | } 178 | -------------------------------------------------------------------------------- /lib/update_servers.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "time" 12 | 13 | "github.com/golang/glog" 14 | ) 15 | 16 | func (s *Server) startUpdatingServerList() { 17 | glog.Infof("Starting to update server list...") 18 | go s.updateServersContinuous() 19 | } 20 | 21 | func (s *Server) updateServersContinuous() { 22 | for { 23 | config := s.GetConfig() 24 | stable, err := config.HostSourcer.GetStableServers() 25 | if err != nil { 26 | glog.Error(err) 27 | } else { 28 | glog.Infof("Adding %d servers to the stable servers list", len(stable)) 29 | if len(stable) > 0 { 30 | s.handleUpdatedList(s.stableServers, stable) 31 | err = config.Algorithm.UpdateStableServerList(stable) 32 | if err != nil { 33 | glog.Errorf("Error updating stable server list: %s", err) 34 | } else { 35 | s.stableServers = stable 36 | } 37 | } 38 | } 39 | 40 | rc, err := config.HostSourcer.GetRCServers() 41 | if err != nil { 42 | glog.Error(err) 43 | } else { 44 | glog.Infof("Adding %d servers to the list of RC servers", len(rc)) 45 | if len(rc) > 0 { 46 | s.handleUpdatedList(s.rcServers, rc) 47 | err = config.Algorithm.UpdateRCServerList(rc) 48 | if err != nil { 49 | glog.Errorf("Error updating RC server list: %s", err) 50 | } else { 51 | s.rcServers = rc 52 | } 53 | } 54 | } 55 | 56 | <-time.NewTimer(config.ServerUpdateInterval).C 57 | } 58 | } 59 | 60 | func (s *Server) handleUpdatedList(old, new []*DHCPServer) { 61 | added, removed := diffServersList(old, new) 62 | if len(added) > 0 || len(removed) > 0 { 63 | glog.Info("Server list updated") 64 | } 65 | } 66 | 67 | type serverKey struct { 68 | // have to store address as string otherwise serverKey can't be used as map key 69 | Address string 70 | Port int 71 | } 72 | 73 | func diffServersList(original, updated []*DHCPServer) (added, removed []*DHCPServer) { 74 | added = make([]*DHCPServer, 0) 75 | removed = make([]*DHCPServer, 0) 76 | 77 | // find servers that were not in original list 78 | originalMap := make(map[serverKey]bool) 79 | for _, s := range original { 80 | key := serverKey{ 81 | s.Address.String(), 82 | s.Port, 83 | } 84 | originalMap[key] = true 85 | } 86 | for _, new := range updated { 87 | key := serverKey{ 88 | new.Address.String(), 89 | new.Port, 90 | } 91 | if _, ok := originalMap[key]; !ok { 92 | added = append(added, new) 93 | } 94 | } 95 | 96 | // find servers that are no longer in the new list 97 | newMap := make(map[serverKey]bool) 98 | for _, s := range updated { 99 | key := serverKey{ 100 | s.Address.String(), 101 | s.Port, 102 | } 103 | newMap[key] = true 104 | } 105 | for _, old := range original { 106 | key := serverKey{ 107 | old.Address.String(), 108 | old.Port, 109 | } 110 | if _, ok := newMap[key]; !ok { 111 | removed = append(removed, old) 112 | } 113 | } 114 | 115 | return added, removed 116 | } 117 | -------------------------------------------------------------------------------- /lib/update_servers_test.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package dhcplb 9 | 10 | import ( 11 | "fmt" 12 | "net" 13 | "reflect" 14 | "testing" 15 | ) 16 | 17 | func TestDiffServerList(t *testing.T) { 18 | for i, tt := range []struct { 19 | original []*DHCPServer 20 | updated []*DHCPServer 21 | }{ 22 | { 23 | original: []*DHCPServer{}, 24 | updated: []*DHCPServer{}, 25 | }, 26 | { 27 | original: []*DHCPServer{}, 28 | updated: []*DHCPServer{ 29 | 30 | { 31 | Address: net.ParseIP("1.2.3.4"), 32 | Port: 1, 33 | }, 34 | { 35 | Address: net.ParseIP("5.6.7.8"), 36 | Port: 2, 37 | }, 38 | }, 39 | }, 40 | { 41 | original: []*DHCPServer{ 42 | { 43 | Address: net.ParseIP("1.2.3.4"), 44 | Port: 1, 45 | }, 46 | { 47 | Address: net.ParseIP("5.6.7.8"), 48 | Port: 2, 49 | }, 50 | }, 51 | updated: []*DHCPServer{}, 52 | }, 53 | { 54 | original: []*DHCPServer{ 55 | { 56 | Address: net.ParseIP("1.2.3.4"), 57 | Port: 1, 58 | }, 59 | }, 60 | updated: []*DHCPServer{ 61 | { 62 | Address: net.ParseIP("5.6.7.8"), 63 | Port: 2, 64 | }, 65 | }, 66 | }, 67 | } { 68 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 69 | added, removed := diffServersList(tt.original, tt.updated) 70 | if !reflect.DeepEqual(added, tt.updated) { 71 | t.Errorf("added %v, updated %v", added, tt.updated) 72 | } 73 | if !reflect.DeepEqual(removed, tt.original) { 74 | t.Errorf("removed %v, original %v", removed, tt.original) 75 | } 76 | }) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | package main 9 | 10 | import ( 11 | "context" 12 | "flag" 13 | "fmt" 14 | "net/http" 15 | _ "net/http/pprof" 16 | 17 | dhcplb "github.com/facebookincubator/dhcplb/lib" 18 | "github.com/golang/glog" 19 | ) 20 | 21 | // Program parameters 22 | var ( 23 | version = flag.Int("version", 4, "Run in v4/v6 mode") 24 | configPath = flag.String("config", "", "Path to JSON config file") 25 | overridesPath = flag.String("overrides", "", "Path to JSON overrides file") 26 | pprofPort = flag.Int("pprof", 0, "Port to run pprof HTTP server on") 27 | serverMode = flag.Bool("server", false, "Run in server mode. The default is relay mode.") 28 | ) 29 | 30 | func main() { 31 | flag.Parse() 32 | flag.Lookup("logtostderr").Value.Set("true") 33 | 34 | ctx, cancel := context.WithCancel(context.Background()) 35 | defer cancel() 36 | 37 | if *configPath == "" { 38 | glog.Fatal("Config file is necessary") 39 | } 40 | 41 | if *pprofPort != 0 { 42 | go func() { 43 | glog.Infof("Started pprof server on port %d", *pprofPort) 44 | err := http.ListenAndServe(fmt.Sprintf(":%d", *pprofPort), nil) 45 | if err != nil { 46 | glog.Fatal("Error starting pprof server: ", err) 47 | } 48 | }() 49 | } 50 | 51 | logger := NewGlogLogger() 52 | 53 | // load initial config 54 | provider := NewDefaultConfigProvider() 55 | config, err := dhcplb.LoadConfig( 56 | *configPath, *overridesPath, *version, provider) 57 | if err != nil { 58 | glog.Fatalf("Failed to load config: %s", err) 59 | } 60 | 61 | // start watching config 62 | configChan, err := dhcplb.WatchConfig( 63 | *configPath, *overridesPath, *version, provider) 64 | if err != nil { 65 | glog.Fatalf("Failed to watch config: %s", err) 66 | } 67 | 68 | server, err := dhcplb.NewServer(config, *serverMode, logger) 69 | if err != nil { 70 | glog.Fatal(err) 71 | } 72 | 73 | // update server config whenever file changes 74 | go func() { 75 | for config := range configChan { 76 | glog.Info("Config changed") 77 | server.SetConfig(config) 78 | } 79 | }() 80 | 81 | glog.Infof("Starting dhcplb in v%d mode", *version) 82 | glog.Fatal(server.ListenAndServe(ctx)) 83 | } 84 | -------------------------------------------------------------------------------- /overrides.json: -------------------------------------------------------------------------------- 1 | { 2 | "v4": { 3 | 4 | }, 5 | "v6": { 6 | 7 | } 8 | } -------------------------------------------------------------------------------- /vagrant/README.md: -------------------------------------------------------------------------------- 1 | # How to setup your test environment with Vagrant 2 | 3 | The instruction below will help you bringing up a virtual lab containing VMs 4 | sharing their own private network(s). 5 | This assumes you are somewhat familiar with 6 | [`vagrant`](https://www.vagrantup.com/). 7 | This has been tested under OSX but it should work find on Linux too. 8 | Please provide feedback or PRs/patches if you find problems. 9 | This instructions are for DHCPv4 only, DHCPv6 will follow soon. 10 | 11 | ## Install dependencies 12 | 13 | First, install `chef-dk` from https://downloads.chef.io/chef-dk/ . 14 | On OSX you can use `brew`: 15 | 16 | ``` 17 | $ brew cask install chef/chef/chefdk 18 | ``` 19 | 20 | Install `vagrant-berkshelf` plugin: 21 | 22 | ``` 23 | $ vagrant plugin install vagrant-berkshelf 24 | $ cd ${PROJECT_ROOT}/vagrant/chef/cookbooks 25 | $ berks install 26 | ``` 27 | 28 | You might need to disable dhcpserver for `vboxnet0` in VirtualBox: 29 | 30 | ``` 31 | $ VBoxManage dhcpserver remove --netname HostInterfaceNetworking-vboxnet0 32 | ``` 33 | 34 | ## Start VMs 35 | 36 | To start all the vms: 37 | 38 | ``` 39 | $ cd ${PROJECT_ROOT}/vagrant/ 40 | $ vagrant up 41 | ``` 42 | 43 | This will bring up the following VMs: 44 | 45 | * `dhcpserver`: a VM running ISC `dhcpd` (both v4 and v6) configured with a 46 | subnet in the private network space. You can start as many as you want by 47 | changing the variable on top of the `Vagrantfile`. 48 | * `dhcplb`: a VM running the `dhcplb` itself, configured to foward traffic to 49 | the above; 50 | * `dhcprelay`: a VM running ISC `dhcrelay`, it intercepts broadcast/multicast 51 | traffic from the client below and relays traffic to the above; 52 | * `dhcpclient`: a VM you can use to run `dhclient`, or `perfdhcp` manually to 53 | test things. It's DISCOVER/SOLICIT messages will be picked up by the 54 | `dhcprelay` instance 55 | 56 | You can ssh into VMs using `vagrant ssh ${vm_name}`. Destroy them with 57 | `vagrant destrory ${vm_name}`. If you find bugs in the `chef` cookbooks or you 58 | want to change something there you can test your `chef` changes using 59 | `vagrant provision ${vm_name}` on a running VM. 60 | 61 | ## Development cycle 62 | 63 | Just edit `dhcplb`'s code on your host machine (the machine running VirtualBox 64 | or whatever VM solution you are using). The root directory of your github 65 | checkout will be mounted into the `dhcplb` VM at 66 | `~/go/src/github.com/facebookincubator/dhcplb`. 67 | 68 | You can compile the binary using: 69 | 70 | ``` 71 | $ cd ~/go/src/github.com/facebookincubator/dhcplb 72 | $ go build 73 | $ sudo mv dhcplb $GOBIN 74 | ``` 75 | 76 | And restart it with: 77 | 78 | ``` 79 | # initctl restart dhcplb 80 | ``` 81 | 82 | Logs will be in `/var/log/upstart/dhcplb.log` (becuase the current Vagrant image 83 | uses a version of Ubuntu using Upstart init replacement). 84 | 85 | On the `dhcpclient` you can initiate dhcp requests using these commands: 86 | 87 | ``` 88 | # perfdhcp -R 1 -4 -r 1200 -p 30 -t 1 -i 192.168.51.104 89 | # dhclient -d -1 -v -pf /run/dhclient.eth1.pid -lf /var/lib/dhcp/dhclient.eth1.leases eth1 90 | ``` 91 | 92 | You will see: 93 | 94 | ``` 95 | root@dhcpclient:~# dhclient -d -1 -v -pf /run/dhclient.eth1.pid -lf 96 | /var/lib/dhcp/dhclient.eth1.leases eth1 97 | Internet Systems Consortium DHCP Client 4.2.4 98 | Copyright 2004-2012 Internet Systems Consortium. 99 | All rights reserved. 100 | For info, please visit https://www.isc.org/software/dhcp/ 101 | 102 | Listening on LPF/eth1/08:00:27:7b:79:94 103 | Sending on LPF/eth1/08:00:27:7b:79:94 104 | Sending on Socket/fallback 105 | DHCPDISCOVER on eth1 to 255.255.255.255 port 67 interval 3 (xid=0xcd1fdb2d) 106 | DHCPREQUEST of 192.168.51.152 on eth1 to 255.255.255.255 port 67 107 | (xid=0x2ddb1fcd) 108 | DHCPOFFER of 192.168.51.152 from 192.168.51.104 109 | DHCPACK of 192.168.51.152 from 192.168.51.104 110 | RTNETLINK answers: File exists 111 | bound to 192.168.51.152 -- renewal in 227 seconds. 112 | ^C 113 | ``` 114 | 115 | And something in the dhcplb logs: 116 | 117 | ``` 118 | I1125 15:54:11.985895 12190 modulo.go:65] List of available stable servers: 119 | I1125 15:54:11.985943 12190 modulo.go:67] 192.168.50.104:67 120 | I1125 15:54:11.985953 12190 modulo.go:67] 192.168.50.105:67 121 | I1125 15:54:16.532833 12190 glog_logger.go:91] client_mac: 08:00:27:7b:79:94, dhcp_server: 192.168.50.104, giaddr: 192.168.51.101, latency_us: 112, server_is_rc: false, source_ip: 192.168.50.101, success: true, type: Discover, version: 4, xid: 0xcd1fdb2d 122 | I1125 15:54:16.534310 12190 glog_logger.go:91] client_mac: 08:00:27:7b:79:94, dhcp_server: 192.168.50.104, giaddr: 192.168.51.101, latency_us: 117, server_is_rc: false, source_ip: 192.168.50.101, success: true, type: Request, version: 4, xid: 0xcd1fdb2d 123 | ``` 124 | 125 | [ISC KEA's 126 | perfdhcp](https://kea.isc.org/wiki/DhcpBenchmarking) utility comes handy so it's 127 | installed for your convenience. 128 | 129 | Should you need to change something in the `dhcprelay` here are some useful 130 | commands: 131 | 132 | ``` 133 | # initctl list 134 | # initctl (stop|start|restart) isc-dhcp-relay 135 | # /usr/sbin/dhcrelay -d -4 -i eth1 -i eth2 192.168.50.104 136 | ``` 137 | 138 | The relay config is in `/etc/default/isc-dhcp-relay`. 139 | 140 | In general you don't need to touch the `dhcpserver` but you need to restart it 141 | you can use: 142 | 143 | ``` 144 | # /etc/init.d/isc-dhcp-server restart 145 | ``` 146 | 147 | The main config is in `/etc/dhcp/dhcpd.conf`. 148 | Subnets are configured like this should you need to change them: 149 | 150 | ``` 151 | subnet 192.168.50.0 netmask 255.255.255.0 {} 152 | subnet 192.168.51.0 netmask 255.255.255.0 {range 192.168.51.220 192.168.51.230;} 153 | ``` 154 | -------------------------------------------------------------------------------- /vagrant/Vagrantfile: -------------------------------------------------------------------------------- 1 | # If you use OSX and Virtual Box You need to run: 2 | # 3 | # $ VBoxManage dhcpserver remove --netname HostInterfaceNetworking-vboxnet0 4 | # 5 | # to remove the VirtualBox internal DHCP server... as it's going to interfeer 6 | # with the your environment. 7 | 8 | VAGRANTFILE_API_VERSION = "2" 9 | NUM_DHCPSERVERS = 2 10 | 11 | # external net => network where lb, relay and dhcp server sit. 12 | EXT_NET_PREFIX = "192.168.50" 13 | # internal net => network where only the client and relay sit. 14 | INT_NET_PREFIX = "192.168.51" 15 | 16 | # Following structure represents the list of nodes, please note that the "ips" 17 | # array is in format [internal ip, external ip]. ORDER MATTERS. 18 | nodes = { 19 | 'dhcprelay' => 20 | {'ips' => ["#{EXT_NET_PREFIX}.101", "#{INT_NET_PREFIX}.101"], 21 | 'roles' => ['role[dhcprelay]']}, 22 | 23 | 'dhcpclient' => 24 | {'ips' => ["#{INT_NET_PREFIX}.102"], 25 | 'roles' => ['role[dhcpclient]']}, 26 | 27 | 'dhcplb' => 28 | {'ips' => ["#{EXT_NET_PREFIX}.103"], 29 | 'roles' => ['role[dhcplb]']}, 30 | } 31 | 32 | # list of dhcpservers, to be used to configure the dhcplb instance. 33 | dhcpservers_ips = [] 34 | start_ip = 104 35 | (1..NUM_DHCPSERVERS).each do |i| 36 | int_ip = "#{INT_NET_PREFIX}.#{start_ip}" 37 | ext_ip = "#{EXT_NET_PREFIX}.#{start_ip}" 38 | start_ip += 1 39 | nodes["dhcpserver#{i}"] = 40 | {'ips' => [int_ip, ext_ip], 'roles' => ['role[dhcpserver]']} 41 | i += 1 42 | dhcpservers_ips.push(ext_ip) 43 | end 44 | nodes['dhcplb']['target_dhcp_servers'] = dhcpservers_ips 45 | 46 | # the dhcplb ip the relay needs to point to 47 | nodes['dhcprelay']['target_dhcplb'] = nodes['dhcplb']['ips'][0] 48 | 49 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 50 | config.vm.box = "ubuntu/trusty64" 51 | config.berkshelf.enabled = true 52 | config.berkshelf.berksfile_path = "chef/cookbooks/Berksfile" 53 | 54 | nodes.each do |name, node| 55 | config.vm.define name do |vm| 56 | vm.vm.hostname = name 57 | 58 | ips = node["ips"] 59 | ips.each do |ip| 60 | vm.vm.network :private_network, ip: ip 61 | end 62 | 63 | if name == "dhcplb" 64 | vm.vm.synced_folder "../", 65 | "/home/vagrant/go/src/github.com/facebookincubator/dhcplb/" 66 | end 67 | 68 | vm.vm.provision :chef_solo do |chef| 69 | chef.arguments = "--chef-license accept" 70 | chef.cookbooks_path = ["chef/cookbooks"] 71 | chef.roles_path = "chef/roles" 72 | chef.add_role("base") 73 | chef.json = nodes 74 | node['roles'].each do |role| 75 | chef.add_role(role) 76 | end 77 | end 78 | 79 | end 80 | end 81 | end 82 | 83 | # -*- mode: ruby -*- 84 | # vi: set ft=ruby : 85 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *# 3 | .#* 4 | \#*# 5 | .*.sw[a-z] 6 | *.un~ 7 | pkg/ 8 | 9 | # Berkshelf 10 | .vagrant 11 | /cookbooks 12 | Berksfile.lock 13 | 14 | # Bundler 15 | Gemfile.lock 16 | bin/* 17 | .bundle/* 18 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/.kitchen.yml: -------------------------------------------------------------------------------- 1 | --- 2 | driver: 3 | name: vagrant 4 | 5 | provisioner: 6 | name: chef_solo 7 | 8 | platforms: 9 | - name: ubuntu-14.04 10 | - name: centos-7.2 11 | 12 | suites: 13 | - name: default 14 | run_list: 15 | attributes: 16 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/Berksfile: -------------------------------------------------------------------------------- 1 | source "https://supermarket.chef.io/" 2 | cookbook 'apt' 3 | cookbook 'golang' 4 | cookbook 'poise-service' 5 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcpclient/README.md: -------------------------------------------------------------------------------- 1 | This cookbook configures a VM containing dhcp clients (like `dhclient` and 2 | `perfdhcp`) 3 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcpclient/metadata.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | name 'dhcpclient' 7 | maintainer 'Facebook' 8 | maintainer_email 'pallotron@fb.com' 9 | license 'All rights reserved' 10 | description 'Installs/Configures the dhcp client VM' 11 | long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) 12 | version '0.1.0' 13 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcpclient/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | apt_repository 'kea-repo' do 7 | uri 'ppa:xdeccardx/isc-kea' 8 | end 9 | 10 | # this contains perfdhcp utility 11 | package 'kea-admin' do 12 | action :install 13 | end 14 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcplb/README.md: -------------------------------------------------------------------------------- 1 | This cookbook configures the dhcplb VM. 2 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcplb/files/default/dhcplb.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "v4": { 3 | "version": 4, 4 | "listen_addr": "0.0.0.0", 5 | "port": 67, 6 | "packet_buf_size": 1024, 7 | "update_server_interval": 30, 8 | "algorithm": "xid", 9 | "host_sourcer": "file:/home/vagrant/dhcp-servers-v4.cfg", 10 | "rc_ratio": 0, 11 | "throttle_cache_size": 1024, 12 | "throttle_cache_rate": 128, 13 | "throttle_rate": 256 14 | }, 15 | "v6": { 16 | "version": 6, 17 | "listen_addr": "::", 18 | "port": 547, 19 | "packet_buf_size": 1024, 20 | "update_server_interval": 30, 21 | "algorithm": "xid", 22 | "host_sourcer": "file:/home/vagrant/dhcp-servers-v6.cfg", 23 | "rc_ratio": 0, 24 | "throttle_cache_size": 1024, 25 | "throttle_cache_rate": 128, 26 | "throttle_rate": 256 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcplb/metadata.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | name 'dhcplb' 7 | maintainer 'Facebook' 8 | maintainer_email 'pallotron@fb.com' 9 | license 'All rights reserved' 10 | description 'Installs/Configures dhcplb' 11 | long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) 12 | version '0.1.0' 13 | depends 'poise-service' 14 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcplb/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | node.default['go']['version'] = '1.13' 7 | node.default['go']['packages'] = ['github.com/facebookincubator/dhcplb'] 8 | 9 | include_recipe 'golang' 10 | include_recipe 'golang::packages' 11 | 12 | directory '/home/vagrant/go' do 13 | owner 'vagrant' 14 | group 'vagrant' 15 | recursive true 16 | end 17 | 18 | cookbook_file '/home/vagrant/dhcplb.config.json' do 19 | source 'dhcplb.config.json' 20 | notifies :restart, 'poise_service[dhcplb]' 21 | end 22 | 23 | template '/home/vagrant/dhcp-servers-v4.cfg' do 24 | source 'dhcp-servers-v4.cfg.erb' 25 | # dhcplb will auto load files that change. no need to notify. 26 | end 27 | 28 | # Configure service via https://github.com/poise/poise-service 29 | poise_service 'dhcplb' do 30 | command '/opt/go/bin/dhcplb -version 4 -config /home/vagrant/dhcplb.config.json' 31 | end 32 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcplb/templates/default/dhcp-servers-v4.cfg.erb: -------------------------------------------------------------------------------- 1 | <% 2 | # Copyright (c) Facebook, Inc. and its affiliates. 3 | # 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | 7 | %> 8 | <%= node['dhcplb']['target_dhcp_servers'].join("\n") %> 9 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcprelay/README.md: -------------------------------------------------------------------------------- 1 | This cookbook configures dhcrelay to point to dhcplb. 2 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcprelay/metadata.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | name 'dhcprelay' 7 | maintainer 'Facebook' 8 | maintainer_email 'pallotron@fb.com' 9 | license 'All rights reserved' 10 | description 'Installs/Configures isc-dhcp-relay' 11 | long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) 12 | version '0.1.0' 13 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcprelay/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | package 'isc-dhcp-relay' do 7 | action :install 8 | end 9 | 10 | template '/etc/default/isc-dhcp-relay' do 11 | source 'etc_default_isc-dhcp-relay.erb' 12 | owner 'root' 13 | group 'root' 14 | mode '0644' 15 | notifies :restart, 'service[isc-dhcp-relay]' 16 | end 17 | 18 | service 'isc-dhcp-relay' do 19 | action [:enable, :start] 20 | end 21 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcprelay/templates/default/etc_default_isc-dhcp-relay.erb: -------------------------------------------------------------------------------- 1 | <% 2 | # Copyright (c) Facebook, Inc. and its affiliates. 3 | # 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | 7 | %> 8 | # Defaults for isc-dhcp-relay initscript 9 | # sourced by /etc/init.d/isc-dhcp-relay 10 | # installed at /etc/default/isc-dhcp-relay by the maintainer scripts 11 | # 12 | # This is a POSIX shell fragment 13 | # 14 | # What servers should the DHCP relay forward requests to? 15 | # Pointing to the IP of the dhcplb 16 | SERVERS="<%= node['dhcprelay']['target_dhcplb'] -%>" 17 | 18 | # On what interfaces should the DHCP relay (dhrelay) serve DHCP requests? 19 | INTERFACES="eth1 eth2" 20 | 21 | # Additional options that are passed to the DHCP relay daemon? 22 | OPTIONS="" 23 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcpserver/README.md: -------------------------------------------------------------------------------- 1 | This cookbook configured isc dhcpd. 2 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcpserver/files/default/etc_default_isc-dhcp-server: -------------------------------------------------------------------------------- 1 | # Defaults for isc-dhcp-server initscript 2 | # sourced by /etc/init.d/isc-dhcp-server 3 | # installed at /etc/default/isc-dhcp-server by the maintainer scripts 4 | 5 | # 6 | # This is a POSIX shell fragment 7 | # 8 | 9 | # Path to dhcpd's config file (default: /etc/dhcp/dhcpd.conf). 10 | #DHCPD_CONF=/etc/dhcp/dhcpd.conf 11 | 12 | # Path to dhcpd's PID file (default: /var/run/dhcpd.pid). 13 | #DHCPD_PID=/var/run/dhcpd.pid 14 | 15 | # Additional options to start dhcpd with. 16 | # Don't use options -cf or -pf here; use DHCPD_CONF/ DHCPD_PID instead 17 | #OPTIONS="" 18 | 19 | # On what interfaces should the DHCP server (dhcpd) serve DHCP requests? 20 | # Separate multiple interfaces with spaces, e.g. "eth0 eth1". 21 | INTERFACES="eth1 eth2" 22 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcpserver/metadata.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | name 'dhcpserver' 7 | maintainer 'Facebook' 8 | maintainer_email 'pallotron@fb.com' 9 | license 'All rights reserved' 10 | description 'Installs/Configures isc-dhcp-server' 11 | long_description IO.read(File.join(File.dirname(__FILE__), 'README.md')) 12 | version '0.1.0' 13 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcpserver/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | package 'isc-dhcp-server' do 7 | action :install 8 | end 9 | 10 | node.default['dhcpserver']['subnets'] = [ 11 | {'subnet' => '192.168.50.0', 'range' => []}, 12 | {'subnet' => '192.168.51.0', 'range' => ['192.168.51.150', '192.168.51.250']} 13 | ] 14 | 15 | template '/etc/dhcp/dhcpd.conf' do 16 | source 'dhcpd.conf.erb' 17 | owner 'root' 18 | group 'root' 19 | mode '0644' 20 | notifies :restart, 'service[isc-dhcp-server]' 21 | end 22 | 23 | cookbook_file '/etc/default/isc-dhcp-server' do 24 | source 'etc_default_isc-dhcp-server' 25 | owner 'root' 26 | group 'root' 27 | mode '0644' 28 | notifies :restart, 'service[isc-dhcp-server]' 29 | end 30 | 31 | 32 | service 'isc-dhcp-server' do 33 | action [:enable, :start] 34 | end 35 | -------------------------------------------------------------------------------- /vagrant/chef/cookbooks/dhcpserver/templates/default/dhcpd.conf.erb: -------------------------------------------------------------------------------- 1 | <% 2 | # Copyright (c) Facebook, Inc. and its affiliates. 3 | # 4 | # This source code is licensed under the MIT license found in the 5 | # LICENSE file in the root directory of this source tree. 6 | 7 | %> 8 | ddns-update-style none; 9 | option domain-name "dhcplb.com"; 10 | option domain-name-servers ns1.example.org, ns2.example.org; 11 | default-lease-time 600; 12 | max-lease-time 7200; 13 | log-facility local7; 14 | <% subnets = node['dhcpserver']['subnets'] -%> 15 | <% subnets.each do |s| -%> 16 | <% if s['range'].size > 0 -%> 17 | subnet <%=s['subnet']-%> netmask 255.255.255.0 {range <%= s['range'][0] -%> <%= s['range'][1]-%>;} 18 | <% else -%> 19 | subnet <%=s['subnet']-%> netmask 255.255.255.0 {} 20 | <% end -%> 21 | <% end -%> 22 | -------------------------------------------------------------------------------- /vagrant/chef/roles/base.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | name 'base' 7 | run_list( 8 | 'recipe[apt]' 9 | ) 10 | -------------------------------------------------------------------------------- /vagrant/chef/roles/dhcpclient.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | name 'dhcpclient' 7 | run_list( 8 | 'recipe[dhcpclient]' 9 | ) 10 | -------------------------------------------------------------------------------- /vagrant/chef/roles/dhcplb.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | name 'dhcplb' 7 | run_list( 8 | 'recipe[dhcplb]', 9 | 'recipe[golang]', 10 | 'recipe[golang::packages]', 11 | ) 12 | -------------------------------------------------------------------------------- /vagrant/chef/roles/dhcprelay.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | name 'dhcprelay' 7 | run_list( 8 | 'recipe[dhcprelay]' 9 | ) 10 | -------------------------------------------------------------------------------- /vagrant/chef/roles/dhcpserver.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) Facebook, Inc. and its affiliates. 2 | # 3 | # This source code is licensed under the MIT license found in the 4 | # LICENSE file in the root directory of this source tree. 5 | 6 | name 'dhcpserver' 7 | run_list( 8 | 'recipe[dhcpserver]' 9 | ) 10 | --------------------------------------------------------------------------------