├── .github └── workflows │ └── goreleaser.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── api └── server.go ├── cmd ├── arpinject.go ├── root.go └── server.go ├── config ├── config.go ├── viper.go └── zerolog.go ├── dhcpd ├── arp │ ├── arp.go │ ├── arp_linux.go │ ├── arp_linux_64.go │ └── arp_notlinux.go ├── handler.go ├── server.go └── types.go ├── examples ├── ubuntu-1804.yml ├── ubuntu-2004-ram.yml └── ubuntu-2004.yml ├── go.mod ├── go.sum ├── httpd ├── handler.go └── server.go ├── main.go ├── manifest ├── io.go ├── ipnet.go ├── mac.go └── schema.go ├── netbootd.service ├── netbootd.yml ├── static ├── README.md ├── files.go ├── ipxe.efi ├── ipxe_arm64.efi └── undionly.kpxe ├── store ├── persistence.go └── store.go └── tftpd ├── handler.go └── server.go /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | goreleaser: 9 | runs-on: ubuntu-20.04 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.17.x 20 | 21 | - name: Run GoReleaser 22 | uses: goreleaser/goreleaser-action@v2 23 | with: 24 | version: latest 25 | args: release --rm-dist 26 | key: ${{ secrets.YOUR_PRIVATE_KEY }} 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | netbootd 3 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: netbootd 2 | before: 3 | hooks: 4 | - go mod download 5 | builds: 6 | - binary: netbootd 7 | # Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser`. 8 | ldflags: 9 | - -s -w -X github.com/DSpeichert/netbootd/cmd.version={{.Version}} -X github.com/DSpeichert/netbootd/cmd.commit={{.ShortCommit}} -X github.com/DSpeichert/netbootd/cmd.date={{.Date}} 10 | goos: 11 | - linux 12 | - darwin 13 | goarch: 14 | - amd64 15 | - arm 16 | - arm64 17 | goarm: 18 | - 7 19 | 20 | archives: 21 | - wrap_in_directory: true 22 | format: tar.gz 23 | 24 | # Additional files/globs you want to add to the archive. 25 | # Defaults are any files matching `LICENCE*`, `LICENSE*`, 26 | # `README*` and `CHANGELOG*` (case-insensitive). 27 | files: 28 | - examples/* 29 | - CHANGELOG* 30 | - README* 31 | - LICENSE* 32 | - netbootd.service 33 | - netbootd.yml 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Daniel Speichert 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netbootd 2 | 3 | netbootd is a lightweight network boot server, designed for maximum flexibility 4 | and with "batteries included" approach in mind, serving as a DHCP, TFTP and HTTP server. 5 | It includes a basic templating functionality, designed to allow generating e.g. preseed 6 | files for unattended OS installation. 7 | 8 | It can be compared to [Foreman](https://github.com/theforeman/foreman) or [Cobbler](https://github.com/cobbler/cobbler), 9 | as the goal is to PXE-boot a machine into an operating system or installation environment. 10 | 11 | Unlike Foreman and Cobbler, netbootd is actually a DHCP, TFTP and HTTP server. 12 | It does not require any other software to be used. 13 | 14 | netbootd aims to provide maximum flexibility and unlike Foreman or Cobbler makes 15 | no attempt to simplify the process of network booting. The results will be only 16 | as good as the configuration (the manifest in this case). 17 | 18 | netbootd's configuration consists of a set of manifests, with each manifest representing a machine 19 | to be provisioned. Netbootd also provides a simple HTTP API for managing manifests, so that netbootd 20 | can become part of a larger automation workflow. 21 | 22 | **Note: This software is highly experimental, at proof-of-concept stage. It works 23 | but a lot of critical features are missing.** 24 | 25 | ## DHCP 26 | 27 | netbootd includes a DHCP server that will respond ONLY to MAC addresses found in 28 | one of the manifests. It does not implement the concept of leases as IPs are implied 29 | to be statically allocated via manifest configuration. 30 | 31 | Multiple options are supported, such as router, hostname, domain, DNS, NTP, 32 | and naturally NBP. 33 | 34 | ## TFTP and HTTP 35 | 36 | netbootd exposes all "mounts" via both TFTP and HTTP simultatenously. 37 | Naturally, it's not a good idea to transfer really large files over TFTP but PXE generally 38 | requires use of TFTP in most cases. 39 | 40 | TFTP and HTTP content can either be static text (embedded in the manifest), generated content (using 41 | Go's `text/template` templating engine) or proxied to upstream HTTP(S). This last feature is mainly intended to proxy 42 | TFTP to HTTP(S) but very well may be used to reverse-proxy HTTP in otherwise isolated environments and can use a proxy 43 | itself 44 | (`HTTP_PROXY` and `NO_PROXY` is honored automatically by Go). 45 | 46 | netbootd can serve local files using the `path.localDir` configuration option. 47 | netbootd also contains a bundled version of [iPXE](https://ipxe.org/), which allows 48 | downloading (typically) kernel and initrd over HTTP instead of TFTP. 49 | 50 | ## Manifests 51 | 52 | A manifest represents a machine to be provisioned/served. The behavior of built-in DHCP, TFTP and HTTP server is 53 | specific to a manifest, meaning that it varies based on source MAC/IP. Each host may see different content 54 | at `/something` path. 55 | 56 | Note that this is not a security feature, and you should not host any sensitive content. MAC and IPs can be easily 57 | spoofed. In fact, netbootd includes a convenience feature to spoof source IP for troubleshooting purposes. 58 | Append `?spoof=` to HTTP request to see the response for a particular host. There is no TFTP counterpart of 59 | this feature. 60 | 61 | Example manifests are included in the `examples/` directory. 62 | 63 | ### Anatomy of a manifest 64 | 65 | ```yaml 66 | --- 67 | # ID can be anything unique, URL-safe, used to identify it for HTTP API 68 | id: ubuntu-1804 69 | 70 | ### DHCP options - used for DHCP responses from netbootd 71 | # IP address with subnet (CIDR) to give out 72 | ipv4: 192.168.17.101/24 73 | # Hostname (without domain part) (Option 12) 74 | hostname: ubuntu-machine-1804 75 | # Domain part (used for hostname) (Option 15) 76 | domain: test.local 77 | # Lease duration is used as Option 51 78 | # Note that netbootd is a static-assignment server, which does not prevent IP conflicts. 79 | leaseDuration: 1h 80 | # The MAC addresses which map to this manifest 81 | # List multiple for machine with multiple NICs, if not sure which one boots first 82 | mac: 83 | - 00:15:5d:bd:be:15 84 | - aa:bb:cc:dd:ee:fc 85 | # Domain name servers (DNS) in the order of preference (Option 6) 86 | dns: 87 | - 1.2.3.4 88 | - 3.4.5.6 89 | # Routers in the order of preference (Option 3), more than one is rare 90 | router: 91 | - 192.168.17.1 92 | # NTP servers in the order of preference (Option 42), IP address required 93 | ntp: 94 | - 192.168.17.1 95 | # Whether a bundled iPXE bootloader should be served first (before bootFilename). 96 | # When iPXE is loaded, it does DHCP again and netbootd detects its client string 97 | # to break the boot loop and serve bootFilename instead. 98 | ipxe: true 99 | # The name of NBP file name, server over TFTP from "next server", 100 | # which netbootd automatically points to be itself. 101 | # This should map to a "mount" below. 102 | bootFilename: install.ipxe 103 | 104 | # Mounts define virtual per-host (per-manifest) paths that are acessible 105 | # over both TFTP and HTTP but only from the IP address of in this manifest. 106 | # Each mount can be either a proxy mount (HTTP/HTTPS proxy) or a content mount (static). 107 | mounts: 108 | - path: /netboot 109 | # When true, all paths starting with this prefix use this mount. 110 | pathIsPrefix: true 111 | # When proxy is defined, these requests are proxied to a HTTP/HTTPS address. 112 | proxy: http://archive.ubuntu.com/ubuntu/dists/bionic-updates/main/installer-amd64/current/images/hwe-netboot/ubuntu-installer/amd64/ 113 | # When true, the proxy path defined above gets a suffix to the Path prefix appended to it. 114 | appendSuffix: true 115 | 116 | - path: /subdir 117 | # When true, all paths starting with this prefix use this mount. 118 | pathIsPrefix: true 119 | # Provides a path on the host to find the files. 120 | # So that localDir: /tftpboot path: /subdir and client request: /subdir/file.x so that the host 121 | # path becomes /tfptboot/file.x 122 | localDir: /tftpboot 123 | # When true, the localDir path defined above gets a suffix to the Path prefix appended to it. 124 | appendSuffix: true 125 | 126 | - path: /install.ipxe 127 | # The templating context provides access to: .LocalIP, .RemoteIP, .HttpBaseUrl and .Manifest. 128 | # Sprig functions are available: masterminds.github.io/sprig 129 | content: | 130 | #!ipxe 131 | # See https://ipxe.org/scripting for iPXE commands/scripting documentation 132 | 133 | set base {{ .HttpBaseUrl }}/netboot 134 | 135 | {{ $hostnameParts := splitList "." .Manifest.Hostname }} 136 | kernel ${base}/linux gfxpayload=800x600x16,800x600 initrd=initrd.gz auto=true url={{ .HttpBaseUrl.String }}/preseed.txt netcfg/get_ipaddress={{ .Manifest.IPv4.IP }} netcfg/get_netmask={{ .Manifest.IPv4.Netmask }} netcfg/get_gateway={{ first .Manifest.Router }} netcfg/get_nameservers="{{ .Manifest.DNS | join " " }}" netcfg/disable_autoconfig=true hostname={{ first $hostnameParts }} domain={{ rest $hostnameParts | join "." }} DEBCONF_DEBUG=developer 137 | initrd ${base}/initrd.gz 138 | boot 139 | ``` 140 | 141 | ## HTTP API 142 | 143 | In this preview/development version, this HTTP API does not support authentication. 144 | 145 |
146 | GET /api/manifests 147 | Returns a dictionary of all manifests keyed by their ID. 148 | 149 | Supports `Accept` header (if provided) that allows selecting a json output (`Accept: application/json`). 150 |
151 | 152 |
153 | GET /api/manifests/{id} 154 | Returns a single manifest with ID provided in the URL path. 155 | 156 | Supports `Accept` header (if provided) that allows selecting a json output (`Accept: application/json`). 157 | 158 | Returns: 159 | 160 | * 200 for successful response 161 | * 404 if manifest with provided ID does not exist 162 | 163 |
164 | 165 |
166 | PUT /api/manifests/{id} 167 | Accepts a manifest in either JSON (`Content-type: application/json`) or YAML (default) format. 168 | 169 | Returns: 170 | 171 | * 201 Created on success 172 | * 400 for malformed request (invalid manifest) 173 | 174 |
175 | 176 |
177 | DELETE /api/manifests/{id} 178 | Ensures that manifest with provided ID does not exist. 179 | 180 | Always returns 204, even if manifest already did not exist. 181 |
182 | 183 |
184 | GET|POST /api/self/suspend-boot 185 | Allows a provisioned host to ask not to be booted again. 186 | This does not block DHCP, TFTP or HTTP requests, it only removes NBP information from DHCP responses. 187 | 188 | This operation looks for a manifest matching the IP address of the requester. It is possible to spoof it 189 | with `?spoof=1.2.3.4` query parameter. 190 |
191 | 192 |
193 | GET|POST /api/self/unsuspend-boot 194 | Re-enables booting for a provisioned host. 195 | 196 | This operation looks for a manifest matching the IP address of the requester. It is possible to spoof it 197 | with `?spoof=1.2.3.4` query parameter. 198 |
199 | 200 |
201 | GET /api/self/manifest 202 | Returns a manifest matching requester's IP Address. 203 | 204 | Supports `Accept` header (if provided) that allows selecting a json output (`Accept: application/json`). 205 | 206 | This operation looks for a manifest matching the IP address of the requester. It is possible to spoof it 207 | with `?spoof=1.2.3.4` query parameter. 208 |
209 | 210 | ## Usage 211 | 212 | ``` 213 | Usage: 214 | netbootd server [flags] 215 | 216 | Flags: 217 | -a, --address string IP address to listen on (DHCP, TFTP, HTTP) 218 | -r, --api-port int HTTP API port to listen on (default 8081) 219 | --api-tls-cert string Path to TLS certificate API 220 | --api-tls-key string Path to TLS certificate for API 221 | -h, --help help for server 222 | -p, --http-port int HTTP port to listen on (default 8080) 223 | -i, --interface string interface to listen on, e.g. eth0 (DHCP) 224 | -m, --manifests string load manifests from directory 225 | 226 | Global Flags: 227 | -d, --debug enable debug logging 228 | --disable-journal-logger disable zerolog journald logger 229 | --trace enable trace logging 230 | ``` 231 | 232 | Run e.g. `./netbootd --trace server -m ./examples/` 233 | 234 | ## Roadmap / TODOs 235 | 236 | * [x] API TLS & Authentication 237 | * [ ] Manifest persistence (currently API-configured manifests live in memory only) 238 | * [ ] Pluggable store backends (e.g. Redis, Etcd, files) for Manifests 239 | * [ ] Notifications (e.g. long-polling wait to return when a given host actually booted) 240 | * [ ] Per-manifest logs available over API 241 | -------------------------------------------------------------------------------- /api/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/DSpeichert/netbootd/manifest" 6 | "github.com/DSpeichert/netbootd/store" 7 | "github.com/gorilla/mux" 8 | "github.com/rs/zerolog" 9 | "github.com/rs/zerolog/log" 10 | "gopkg.in/yaml.v2" 11 | "io/ioutil" 12 | "net" 13 | "net/http" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | type Server struct { 19 | router *mux.Router 20 | httpServer *http.Server 21 | 22 | logger zerolog.Logger 23 | store *store.Store 24 | } 25 | 26 | // NewServer set up HTTP API server instance 27 | // If authorization is passed, requires privileged operation callers to present Authorization header with this content. 28 | func NewServer(store *store.Store, authorization string) (server *Server, err error) { 29 | r := mux.NewRouter() 30 | 31 | server = &Server{ 32 | router: r, 33 | httpServer: &http.Server{ 34 | Handler: r, 35 | WriteTimeout: 10 * time.Second, 36 | ReadTimeout: 10 * time.Second, 37 | MaxHeaderBytes: 1 << 20, 38 | IdleTimeout: 10 * time.Second, 39 | }, 40 | logger: log.With().Str("service", "api").Logger(), 41 | store: store, 42 | } 43 | 44 | // custom server header 45 | r.Use(func(next http.Handler) http.Handler { 46 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 47 | w.Header().Set("Server", "netbootd") 48 | next.ServeHTTP(w, r) 49 | }) 50 | }) 51 | 52 | // custom logging middleware 53 | r.Use(func(next http.Handler) http.Handler { 54 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 55 | start := time.Now() 56 | next.ServeHTTP(w, r) 57 | stop := time.Now() 58 | server.logger.Info(). 59 | Int64("latency", stop.Sub(start).Microseconds()). 60 | Str("ip", r.RemoteAddr). 61 | Str("uri", r.RequestURI). 62 | Str("method", r.Method). 63 | Msg("request completed") 64 | }) 65 | }) 66 | 67 | // GET /api/manifests/{id} 68 | r.HandleFunc("/api/manifests/{id}", func(w http.ResponseWriter, r *http.Request) { 69 | if authorization != r.Header.Get("Authorization") { 70 | http.Error(w, "Forbidden", http.StatusForbidden) 71 | return 72 | } 73 | 74 | vars := mux.Vars(r) 75 | m := store.Find(vars["id"]) 76 | if m == nil { 77 | http.Error(w, "not found", http.StatusNotFound) 78 | return 79 | } 80 | var b []byte 81 | if strings.Contains(r.Header.Get("Accept"), "application/json") { 82 | w.Header().Set("Content-Type", "applications/json") 83 | b, _ = json.Marshal(m) 84 | } else { 85 | w.Header().Set("Content-Type", "text/yaml") 86 | b, _ = yaml.Marshal(m) 87 | } 88 | w.WriteHeader(http.StatusOK) 89 | w.Write(b) 90 | }).Methods("GET") 91 | 92 | // GET /api/manifests 93 | r.HandleFunc("/api/manifests", func(w http.ResponseWriter, r *http.Request) { 94 | if authorization != r.Header.Get("Authorization") { 95 | http.Error(w, "Forbidden", http.StatusForbidden) 96 | return 97 | } 98 | 99 | var b []byte 100 | if strings.Contains(r.Header.Get("Accept"), "application/json") { 101 | w.Header().Set("Content-Type", "applications/json") 102 | b, _ = json.Marshal(store.GetAll()) 103 | } else { 104 | w.Header().Set("Content-Type", "text/yaml") 105 | b, _ = yaml.Marshal(store.GetAll()) 106 | } 107 | w.WriteHeader(http.StatusOK) 108 | w.Write(b) 109 | }).Methods("GET") 110 | 111 | // PUT /api/manifests/{id} 112 | r.HandleFunc("/api/manifests/{id}", func(w http.ResponseWriter, r *http.Request) { 113 | if authorization != r.Header.Get("Authorization") { 114 | http.Error(w, "Forbidden", http.StatusForbidden) 115 | return 116 | } 117 | 118 | buf, _ := ioutil.ReadAll(r.Body) 119 | var m manifest.Manifest 120 | if r.Header.Get("Content-Type") == "application/json" { 121 | err = json.Unmarshal(buf, &m) 122 | if err != nil { 123 | http.Error(w, err.Error(), http.StatusBadRequest) 124 | return 125 | } 126 | } else { 127 | m, err = manifest.ManifestFromYaml(buf) 128 | if err != nil { 129 | http.Error(w, err.Error(), http.StatusBadRequest) 130 | return 131 | } 132 | } 133 | _ = store.PutManifest(m) 134 | w.WriteHeader(http.StatusCreated) 135 | }).Methods("PUT") 136 | 137 | // DELETE /api/manifests/{id} 138 | r.HandleFunc("/api/manifests/{id}", func(w http.ResponseWriter, r *http.Request) { 139 | if authorization != r.Header.Get("Authorization") { 140 | http.Error(w, "Forbidden", http.StatusForbidden) 141 | return 142 | } 143 | 144 | vars := mux.Vars(r) 145 | store.ForgetManifest(vars["id"]) 146 | 147 | w.WriteHeader(http.StatusNoContent) 148 | }).Methods("DELETE") 149 | 150 | // GET|POST /api/self/suspend-boot 151 | r.HandleFunc("/api/self/suspend-boot", func(w http.ResponseWriter, r *http.Request) { 152 | var ip net.IP 153 | if queryFirst(r, "spoof") != "" { 154 | if authorization != r.Header.Get("Authorization") { 155 | http.Error(w, "Forbidden", http.StatusForbidden) 156 | return 157 | } 158 | ip = net.ParseIP(queryFirst(r, "spoof")) 159 | } else { 160 | host, _, _ := net.SplitHostPort(r.RemoteAddr) 161 | ip = net.ParseIP(host) 162 | } 163 | 164 | m := server.store.FindByIP(ip) 165 | if m == nil { 166 | http.Error(w, "not found", http.StatusNotFound) 167 | return 168 | } 169 | m.Suspended = true 170 | 171 | w.WriteHeader(http.StatusOK) 172 | }).Methods("GET", "POST") 173 | 174 | // GET|POST /api/self/unsuspend-boot 175 | r.HandleFunc("/api/self/unsuspend-boot", func(w http.ResponseWriter, r *http.Request) { 176 | var ip net.IP 177 | if queryFirst(r, "spoof") != "" { 178 | if authorization != r.Header.Get("Authorization") { 179 | http.Error(w, "Forbidden", http.StatusForbidden) 180 | return 181 | } 182 | ip = net.ParseIP(queryFirst(r, "spoof")) 183 | } else { 184 | host, _, _ := net.SplitHostPort(r.RemoteAddr) 185 | ip = net.ParseIP(host) 186 | } 187 | 188 | m := server.store.FindByIP(ip) 189 | if m == nil { 190 | http.Error(w, "not found", http.StatusNotFound) 191 | return 192 | } 193 | m.Suspended = false 194 | 195 | w.WriteHeader(http.StatusOK) 196 | }).Methods("GET", "POST") 197 | 198 | // GET /api/self/manifest 199 | r.HandleFunc("/api/self/manifest", func(w http.ResponseWriter, r *http.Request) { 200 | var ip net.IP 201 | if queryFirst(r, "spoof") != "" { 202 | if authorization != r.Header.Get("Authorization") { 203 | http.Error(w, "Forbidden", http.StatusForbidden) 204 | return 205 | } 206 | ip = net.ParseIP(queryFirst(r, "spoof")) 207 | } else { 208 | host, _, _ := net.SplitHostPort(r.RemoteAddr) 209 | ip = net.ParseIP(host) 210 | } 211 | 212 | m := server.store.FindByIP(ip) 213 | if m == nil { 214 | http.Error(w, "not found", http.StatusNotFound) 215 | return 216 | } 217 | var b []byte 218 | if strings.Contains(r.Header.Get("Accept"), "application/json") { 219 | w.Header().Set("Content-Type", "applications/json") 220 | b, _ = json.Marshal(store.GetAll()) 221 | } else { 222 | w.Header().Set("Content-Type", "text/yaml") 223 | b, _ = yaml.Marshal(store.GetAll()) 224 | } 225 | w.WriteHeader(http.StatusOK) 226 | w.Write(b) 227 | }).Methods("GET") 228 | 229 | return server, nil 230 | } 231 | 232 | func (server *Server) Serve(l net.Listener) error { 233 | return server.httpServer.Serve(l) 234 | } 235 | 236 | func (server *Server) ServeTLS(l net.Listener, certFile string, keyFile string) error { 237 | return server.httpServer.ServeTLS(l, certFile, keyFile) 238 | } 239 | 240 | func queryFirst(r *http.Request, k string) string { 241 | keys, ok := r.URL.Query()[k] 242 | if !ok || len(keys[0]) < 1 { 243 | return "" 244 | } 245 | return keys[0] 246 | } 247 | -------------------------------------------------------------------------------- /cmd/arpinject.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/DSpeichert/netbootd/config" 7 | "github.com/DSpeichert/netbootd/dhcpd/arp" 8 | "github.com/rs/zerolog/log" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var ( 13 | mac string 14 | ip string 15 | device string 16 | ) 17 | 18 | func init() { 19 | arpInjectCmd.Flags().StringVarP(&ip, "ip", "i", "", "IP Address") 20 | arpInjectCmd.Flags().StringVarP(&mac, "mac", "m", "", "MAC address") 21 | arpInjectCmd.Flags().StringVarP(&device, "device", "d", "", "device") 22 | 23 | //rootCmd.AddCommand(arpInjectCmd) 24 | } 25 | 26 | var arpInjectCmd = &cobra.Command{ 27 | Use: "arpinject", 28 | Run: func(cmd *cobra.Command, args []string) { 29 | config.InitZeroLog() 30 | parsedIp := net.ParseIP(ip) 31 | parsedMac, err := net.ParseMAC(mac) 32 | if err != nil { 33 | log.Error(). 34 | Err(err). 35 | Msg("cannot parse mac") 36 | } 37 | if err = arp.InjectArp(parsedIp, parsedMac, arp.ATF_COM, device); err != nil { 38 | log.Error(). 39 | Err(err). 40 | Msg("cannot inject arp entry") 41 | } 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/DSpeichert/netbootd/config" 8 | "github.com/spf13/cobra" 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | var ( 13 | debug bool 14 | trace bool 15 | version string 16 | commit string 17 | date string 18 | ) 19 | 20 | func init() { 21 | cobra.OnInitialize(config.InitConfig) 22 | rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "enable debug logging") 23 | viper.BindPFlag("debug", rootCmd.Flags().Lookup("debug")) 24 | 25 | rootCmd.PersistentFlags().BoolVar(&trace, "trace", false, "enable trace logging") 26 | viper.BindPFlag("trace", rootCmd.Flags().Lookup("trace")) 27 | 28 | rootCmd.PersistentFlags().BoolVar(&config.ZeroLogJournalDEnabled, "disable-journal-logger", false, "disable zerolog journald logger") 29 | viper.BindPFlag("disable-journal-logger", rootCmd.Flags().Lookup("disable-journal-logger")) 30 | } 31 | 32 | var rootCmd = &cobra.Command{ 33 | Use: "netbootd", 34 | Short: "netbootd is a DHCP/TFTP/HTTP minion", 35 | Long: `A programmable all-inclusive provisioning server including DHCP, TFTP and HTTP capability. 36 | Unlike heavy, complex solutions like Foreman, netbootd is very lightweight and without many features, 37 | allows for complete flexibility in provisioning machines.`, 38 | Version: version + " (" + commit + ") built " + date, 39 | } 40 | 41 | func Execute() { 42 | if err := rootCmd.Execute(); err != nil { 43 | config.InitZeroLog() 44 | fmt.Println(err) 45 | os.Exit(1) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /cmd/server.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "os/signal" 7 | 8 | "github.com/DSpeichert/netbootd/api" 9 | "github.com/DSpeichert/netbootd/config" 10 | "github.com/DSpeichert/netbootd/dhcpd" 11 | "github.com/DSpeichert/netbootd/httpd" 12 | "github.com/DSpeichert/netbootd/store" 13 | "github.com/DSpeichert/netbootd/tftpd" 14 | systemd "github.com/coreos/go-systemd/daemon" 15 | "github.com/rs/zerolog" 16 | "github.com/rs/zerolog/log" 17 | "github.com/spf13/cobra" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | var ( 22 | addr string 23 | ifname string 24 | httpPort int 25 | apiPort int 26 | apiTlsCert string 27 | apiTlsKey string 28 | manifestPath string 29 | ) 30 | 31 | func init() { 32 | serverCmd.Flags().StringVarP(&addr, "address", "a", "", "IP address to listen on (DHCP, TFTP, HTTP)") 33 | viper.BindPFlag("address", serverCmd.Flags().Lookup("address")) 34 | 35 | serverCmd.Flags().IntVarP(&httpPort, "http-port", "p", 8080, "HTTP port to listen on") 36 | viper.BindPFlag("http.port", serverCmd.Flags().Lookup("http-port")) 37 | 38 | serverCmd.Flags().IntVarP(&apiPort, "api-port", "r", 8081, "HTTP API port to listen on") 39 | viper.BindPFlag("api.port", serverCmd.Flags().Lookup("api-port")) 40 | 41 | serverCmd.Flags().StringVar(&apiTlsCert, "api-tls-cert", "", "Path to TLS certificate API") 42 | viper.BindPFlag("api.TLSCertificatePath", serverCmd.Flags().Lookup("api-tls-cert")) 43 | 44 | serverCmd.Flags().StringVar(&apiTlsKey, "api-tls-key", "", "Path to TLS certificate for API") 45 | viper.BindPFlag("api.TLSPrivateKeyPath", serverCmd.Flags().Lookup("api-tls-key")) 46 | 47 | serverCmd.Flags().StringVarP(&ifname, "interface", "i", "", "interface to listen on, e.g. eth0 (DHCP)") 48 | viper.BindPFlag("interface", serverCmd.Flags().Lookup("interface")) 49 | 50 | serverCmd.Flags().StringVarP(&manifestPath, "manifests", "m", "", "load manifests from directory") 51 | viper.BindPFlag("manifestPath", serverCmd.Flags().Lookup("manifests")) 52 | 53 | rootCmd.AddCommand(serverCmd) 54 | } 55 | 56 | var serverCmd = &cobra.Command{ 57 | Use: "server", 58 | Run: func(cmd *cobra.Command, args []string) { 59 | // configure logging 60 | config.InitZeroLog() 61 | if viper.GetBool("trace") { 62 | zerolog.SetGlobalLevel(zerolog.TraceLevel) 63 | } else if viper.GetBool("debug") { 64 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 65 | } else { 66 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 67 | } 68 | 69 | // set up store 70 | store, _ := store.NewStore(store.Config{ 71 | // TODO: config 72 | PersistenceDirectory: "", 73 | }) 74 | if viper.GetString("manifestPath") != "" { 75 | log.Info().Str("path", viper.GetString("manifestPath")).Msg("Loading manifests") 76 | _ = store.LoadFromDirectory(viper.GetString("manifestPath")) 77 | } 78 | store.GlobalHints.HttpPort = viper.GetInt("http.port") 79 | 80 | // DHCP 81 | dhcpServer, err := dhcpd.NewServer(viper.GetString("address"), viper.GetString("interface"), store) 82 | if err != nil { 83 | log.Fatal().Err(err) 84 | } 85 | go dhcpServer.Serve() 86 | 87 | // TFTP 88 | tftpServer, err := tftpd.NewServer(store) 89 | if err != nil { 90 | log.Fatal().Err(err) 91 | } 92 | connTftp, err := net.ListenUDP("udp", &net.UDPAddr{ 93 | IP: net.ParseIP(viper.GetString("address")), 94 | Port: 69, // TFTP 95 | }) 96 | if err != nil { 97 | log.Fatal().Err(err) 98 | } 99 | go tftpServer.Serve(connTftp) 100 | 101 | // HTTP service 102 | httpServer, err := httpd.NewServer(store) 103 | if err != nil { 104 | log.Fatal().Err(err) 105 | } 106 | connHttp, err := net.ListenTCP("tcp", &net.TCPAddr{ 107 | IP: net.ParseIP(viper.GetString("address")), 108 | Port: viper.GetInt("http.port"), // HTTP 109 | }) 110 | if err != nil { 111 | log.Fatal().Err(err) 112 | } 113 | go httpServer.Serve(connHttp) 114 | log.Info().Interface("addr", connHttp.Addr()).Msg("HTTP listening") 115 | 116 | // HTTP API service 117 | apiServer, err := api.NewServer(store, viper.GetString("api.authorization")) 118 | if err != nil { 119 | log.Fatal().Err(err) 120 | } 121 | connApi, err := net.ListenTCP("tcp", &net.TCPAddr{ 122 | IP: net.ParseIP(viper.GetString("address")), 123 | Port: viper.GetInt("api.port"), // HTTP 124 | }) 125 | if err != nil { 126 | log.Fatal().Err(err) 127 | } 128 | if viper.GetString("api.TLSCertificatePath") != "" && viper.GetString("api.TLSPrivateKeyPath") != "" { 129 | log.Info().Interface("api", connApi.Addr()).Msg("HTTP API listening with TLS...") 130 | go func() { 131 | err := apiServer.ServeTLS(connApi, viper.GetString("api.TLSCertificatePath"), viper.GetString("api.TLSPrivateKeyPath")) 132 | log.Error().Err(err).Msg("Error initializing TLS HTTP API listener!") 133 | }() 134 | } else { 135 | go apiServer.Serve(connApi) 136 | log.Info().Interface("api", connApi.Addr()).Msg("HTTP API listening...") 137 | go func() { 138 | err := apiServer.Serve(connApi) 139 | log.Error().Err(err).Msg("Error initializing HTTP API listener!") 140 | }() 141 | } 142 | if !viper.IsSet("api.authorization") { 143 | log.Warn().Interface("api", connApi.Addr()).Msg("API is running without authentication, set Authorization in config!") 144 | } 145 | 146 | // notify systemd 147 | sent, err := systemd.SdNotify(true, "READY=1\n") 148 | if err != nil { 149 | log.Debug().Err(err).Msg("unable to send systemd daemon successful start message") 150 | } else if sent { 151 | log.Debug().Msg("systemd was notified.") 152 | } else { 153 | log.Debug().Msg("systemd notifications are not supported.") 154 | } 155 | 156 | sigs := make(chan os.Signal, 1) 157 | signal.Notify(sigs, os.Interrupt) 158 | <-sigs 159 | }, 160 | } 161 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | Api struct { 5 | Authorization string 6 | TLSPrivateKeyPath string 7 | TLSCertificatePath string 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /config/viper.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/fsnotify/fsnotify" 5 | "github.com/rs/zerolog/log" 6 | "github.com/spf13/viper" 7 | ) 8 | 9 | var config Config 10 | 11 | // https://github.com/spf13/viper 12 | func InitConfig() { 13 | viper.SetConfigName("netbootd") 14 | viper.SetConfigType("yaml") 15 | viper.AddConfigPath("/etc/netbootd/") 16 | viper.AddConfigPath("$HOME/.config/netbootd/") 17 | viper.AddConfigPath(".") 18 | 19 | viper.SetDefault("store.path", "/var/lib/netbootd") 20 | 21 | viper.SetEnvPrefix("netbootd") 22 | viper.AutomaticEnv() 23 | 24 | err := viper.ReadInConfig() 25 | if err != nil { 26 | InitZeroLog() 27 | log.Debug(). 28 | Err(err). 29 | Msg("error reading config file") 30 | } 31 | } 32 | 33 | // Read (or re-read) the config from external source 34 | func Read() error { 35 | // https://github.com/spf13/viper#unmarshaling to struct 36 | return viper.Unmarshal(&config) 37 | } 38 | 39 | // Get copy of running config 40 | func GetConfig() Config { 41 | return config 42 | } 43 | 44 | func Watch() { 45 | viper.WatchConfig() 46 | viper.OnConfigChange(func(e fsnotify.Event) { 47 | log.Info(). 48 | Str("path", e.Name). 49 | Msg("Config file reloaded") 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /config/zerolog.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io" 5 | "os" 6 | 7 | "github.com/rs/zerolog" 8 | "github.com/rs/zerolog/journald" 9 | "github.com/rs/zerolog/log" 10 | ) 11 | 12 | var ( 13 | zerologInitDone bool // to prevent zerolog to be initialized twice in specific situations (like parsing error of viper configuration file) 14 | ZeroLogJournalDEnabled bool // used by viper to store the status of the --disable-journal-logger flag 15 | ) 16 | 17 | func init() { 18 | // UNIX Time is faster and smaller than most timestamps 19 | // If you set zerolog.TimeFieldFormat to an empty string, 20 | // logs will write with UNIX time 21 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix 22 | } 23 | 24 | func InitZeroLog() { 25 | if !zerologInitDone { 26 | if ZeroLogJournalDEnabled { 27 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) 28 | log.Debug().Msg("Enabled console writer") 29 | } else { 30 | journalWriter := journald.NewJournalDWriter() 31 | multi := io.MultiWriter(zerolog.ConsoleWriter{Out: os.Stderr}, journalWriter) 32 | log.Logger = log.Output(multi) 33 | log.Debug().Msg("Enabled journald writer") 34 | } 35 | zerologInitDone = true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /dhcpd/arp/arp.go: -------------------------------------------------------------------------------- 1 | package arp 2 | 3 | import ( 4 | "syscall" 5 | ) 6 | 7 | // ARP Flag values 8 | // these are not in golang.org/x/sys/unix 9 | const ( 10 | // completed entry (ha valid) 11 | ATF_COM = 0x02 12 | // permanent entry 13 | ATF_PERM = 0x04 14 | // publish entry 15 | ATF_PUBL = 0x08 16 | // has requested trailers 17 | ATF_USETRAILERS = 0x10 18 | // want to use a netmask (only for proxy entries) 19 | ATF_NETMASK = 0x20 20 | // don't answer this addresses 21 | ATF_DONTPUB = 0x40 22 | ) 23 | 24 | // https://man7.org/linux/man-pages/man7/arp.7.html 25 | type arpReq struct { 26 | ArpPa syscall.RawSockaddrInet4 27 | ArpHa syscall.RawSockaddr 28 | Flags int32 29 | Netmask syscall.RawSockaddr 30 | Dev [16]byte 31 | } 32 | -------------------------------------------------------------------------------- /dhcpd/arp/arp_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux && !amd64 && !arm64 2 | // +build linux,!amd64,!arm64 3 | 4 | package arp 5 | 6 | import ( 7 | "net" 8 | "os" 9 | "syscall" 10 | "unsafe" 11 | 12 | "golang.org/x/sys/unix" 13 | ) 14 | 15 | // InjectArp injects an ARP entry into dev's ARP table 16 | // syscalls roughly based on https://www.unix.com/302447674-post3.html 17 | // see: 18 | // https://github.com/torvalds/linux/blob/8cf8821e15cd553339a5b48ee555a0439c2b2742/net/ipv4/arp.c#L1179 19 | // https://github.com/torvalds/linux/blob/8cf8821e15cd553339a5b48ee555a0439c2b2742/net/ipv4/arp.c#L1024 20 | func InjectArp(ip net.IP, mac net.HardwareAddr, flags int32, dev string) (err error) { 21 | fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_UDP) 22 | if err != nil { 23 | return 24 | } 25 | f := os.NewFile(uintptr(fd), "") 26 | defer f.Close() 27 | 28 | return InjectArpFd(uintptr(fd), ip, mac, flags, dev) 29 | } 30 | 31 | func InjectArpFd(fd uintptr, ip net.IP, mac net.HardwareAddr, flags int32, dev string) (err error) { 32 | arpReq := arpReq{ 33 | ArpPa: syscall.RawSockaddrInet4{ 34 | Family: syscall.AF_INET, 35 | }, 36 | //Flags: 0x02 | 0x04, // ATF_COM | ATF_PERM; 37 | Flags: flags, 38 | } 39 | copy(arpReq.ArpPa.Addr[:], ip.To4()) 40 | copy(arpReq.ArpHa.Data[:], mac) 41 | copy(arpReq.Dev[:], dev) 42 | 43 | _, _, errno := unix.Syscall(unix.SYS_IOCTL, fd, unix.SIOCSARP, uintptr(unsafe.Pointer(&arpReq))) 44 | if errno != 0 { 45 | return errno 46 | } 47 | 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /dhcpd/arp/arp_linux_64.go: -------------------------------------------------------------------------------- 1 | //go:build linux && (amd64 || arm64) 2 | // +build linux 3 | // +build amd64 arm64 4 | 5 | package arp 6 | 7 | import ( 8 | "net" 9 | "os" 10 | "syscall" 11 | "unsafe" 12 | 13 | "golang.org/x/sys/unix" 14 | ) 15 | 16 | // InjectArp injects an ARP entry into dev's ARP table 17 | // syscalls roughly based on https://www.unix.com/302447674-post3.html 18 | // see: 19 | // https://github.com/torvalds/linux/blob/8cf8821e15cd553339a5b48ee555a0439c2b2742/net/ipv4/arp.c#L1179 20 | // https://github.com/torvalds/linux/blob/8cf8821e15cd553339a5b48ee555a0439c2b2742/net/ipv4/arp.c#L1024 21 | func InjectArp(ip net.IP, mac net.HardwareAddr, flags int32, dev string) (err error) { 22 | fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_UDP) 23 | if err != nil { 24 | return 25 | } 26 | f := os.NewFile(uintptr(fd), "") 27 | defer f.Close() 28 | 29 | return InjectArpFd(uintptr(fd), ip, mac, flags, dev) 30 | } 31 | 32 | func InjectArpFd(fd uintptr, ip net.IP, mac net.HardwareAddr, flags int32, dev string) (err error) { 33 | arpReq := arpReq{ 34 | ArpPa: syscall.RawSockaddrInet4{ 35 | Family: syscall.AF_INET, 36 | }, 37 | //Flags: 0x02 | 0x04, // ATF_COM | ATF_PERM; 38 | Flags: flags, 39 | } 40 | copy(arpReq.ArpPa.Addr[:], ip.To4()) 41 | 42 | // uint8 to int8 conversion 43 | for i, b := range mac { 44 | arpReq.ArpHa.Data[i] = int8(b) 45 | } 46 | copy(arpReq.Dev[:], dev) 47 | 48 | _, _, errno := unix.Syscall(unix.SYS_IOCTL, fd, unix.SIOCSARP, uintptr(unsafe.Pointer(&arpReq))) 49 | if errno != 0 { 50 | return errno 51 | } 52 | 53 | return 54 | } 55 | -------------------------------------------------------------------------------- /dhcpd/arp/arp_notlinux.go: -------------------------------------------------------------------------------- 1 | //go:build !linux 2 | // +build !linux 3 | 4 | package arp 5 | 6 | import ( 7 | "errors" 8 | "net" 9 | ) 10 | 11 | // InjectArp injects an ARP entry into dev's ARP table 12 | func InjectArp(ip net.IP, mac net.HardwareAddr, flags int32, dev string) (err error) { 13 | return errors.New("not implemented") 14 | } 15 | 16 | func InjectArpFd(fd uintptr, ip net.IP, mac net.HardwareAddr, flags int32, dev string) (err error) { 17 | return errors.New("not implemented") 18 | } 19 | -------------------------------------------------------------------------------- /dhcpd/handler.go: -------------------------------------------------------------------------------- 1 | // Package dhcpd contains snippets from MIT-licensed coredhcp project at 2 | // https://github.com/coredhcp/coredhcp 3 | package dhcpd 4 | 5 | import ( 6 | "errors" 7 | "net" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/DSpeichert/netbootd/dhcpd/arp" 12 | mfest "github.com/DSpeichert/netbootd/manifest" 13 | "github.com/insomniacslk/dhcp/dhcpv4" 14 | "golang.org/x/net/ipv4" 15 | ) 16 | 17 | func (server *Server) HandleMsg4(buf []byte, oob *ipv4.ControlMessage, peer net.Addr) { 18 | var ( 19 | resp *dhcpv4.DHCPv4 20 | err error 21 | bootFileSize int 22 | manifest *mfest.Manifest 23 | ) 24 | 25 | req, err := dhcpv4.FromBytes(buf) 26 | if err != nil { 27 | server.logger.Error().Err(err).Msg("Error parsing DHCPv4 request") 28 | return 29 | } 30 | 31 | server.logger.Trace(). 32 | Str("peer", peer.String()). 33 | Interface("request", req.Summary()). 34 | Msg("Received DHCP packet") 35 | 36 | if req.OpCode != dhcpv4.OpcodeBootRequest { 37 | server.logger.Error(). 38 | Int("opcode", int(req.OpCode)). 39 | Msg("unsupported opcode") 40 | return 41 | } 42 | 43 | resp, err = dhcpv4.NewReplyFromRequest(req) 44 | if err != nil { 45 | server.logger.Error(). 46 | Err(err). 47 | Msg("failed to build reply") 48 | return 49 | } 50 | 51 | switch mt := req.MessageType(); mt { 52 | case dhcpv4.MessageTypeDiscover: 53 | resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeOffer)) 54 | case dhcpv4.MessageTypeRequest: 55 | resp.UpdateOption(dhcpv4.OptMessageType(dhcpv4.MessageTypeAck)) 56 | case dhcpv4.MessageTypeRelease: 57 | return 58 | default: 59 | server.logger.Error(). 60 | Str("type", mt.String()). 61 | Msg("unknown message type") 62 | return 63 | } 64 | 65 | // find local IP 66 | ifIndex := server.Interface.Index 67 | if ifIndex == 0 && oob != nil { 68 | ifIndex = oob.IfIndex 69 | } 70 | localIp, err := getIpv4ForInterface(ifIndex) 71 | if err != nil { 72 | server.logger.Error(). 73 | Err(err). 74 | Int("ifIndex", ifIndex). 75 | Msg("failed to find local interface") 76 | resp = nil 77 | goto response 78 | } 79 | 80 | manifest = server.store.FindByMAC(req.ClientHWAddr) 81 | if manifest == nil { 82 | server.logger.Info(). 83 | Str("MAC", req.ClientHWAddr.String()). 84 | Msg("ignore packet from unknown MAC") 85 | resp = nil 86 | goto response 87 | } 88 | 89 | // server ID 90 | if req.ServerIPAddr != nil && 91 | !req.ServerIPAddr.Equal(net.IPv4zero) && 92 | !req.ServerIPAddr.Equal(localIp) { 93 | server.logger.Trace(). 94 | Msg("requested server ID does not match this server's ID") 95 | resp = nil 96 | goto response 97 | } else { 98 | resp.ServerIPAddr = make(net.IP, net.IPv4len) 99 | copy(resp.ServerIPAddr[:], localIp) 100 | resp.UpdateOption(dhcpv4.OptServerIdentifier(localIp)) 101 | } 102 | 103 | resp.YourIPAddr = manifest.IPv4.IP 104 | resp.Options.Update(dhcpv4.OptSubnetMask(manifest.IPv4.Net.Mask)) 105 | 106 | // lease time 107 | if req.OpCode == dhcpv4.OpcodeBootRequest && manifest.LeaseDuration != 0 { 108 | resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(manifest.LeaseDuration)) 109 | } 110 | 111 | // hostname 112 | if req.IsOptionRequested(dhcpv4.OptionHostName) { 113 | resp.Options.Update(dhcpv4.OptHostName(manifest.Hostname)) 114 | } 115 | 116 | // dns 117 | if req.IsOptionRequested(dhcpv4.OptionDomainNameServer) { 118 | resp.Options.Update(dhcpv4.OptDNS(manifest.DNS...)) 119 | } 120 | 121 | // router 122 | if req.IsOptionRequested(dhcpv4.OptionRouter) { 123 | resp.Options.Update(dhcpv4.OptRouter(manifest.Router...)) 124 | } 125 | 126 | // NTP 127 | if req.IsOptionRequested(dhcpv4.OptionNTPServers) { 128 | resp.Options.Update(dhcpv4.OptNTPServers(manifest.NTP...)) 129 | } 130 | 131 | // NBP 132 | if req.IsOptionRequested(dhcpv4.OptionTFTPServerName) && !manifest.Suspended { 133 | resp.Options.Update(dhcpv4.OptTFTPServerName(localIp.String())) 134 | } 135 | 136 | if req.IsOptionRequested(dhcpv4.OptionBootfileName) && !manifest.Suspended { 137 | // serve iPXE script if user-class is iPXE, or whatever the user chooses if iPXE is disabled 138 | if stringSlicesEqual(req.UserClass(), []string{"iPXE"}) || !manifest.Ipxe { 139 | resp.Options.Update(dhcpv4.OptBootFileName(manifest.BootFilename)) 140 | } else if len(req.ClientArch()) > 0 && req.ClientArch()[0] > 0 { 141 | // likely UEFI (not BIOS) 142 | if strings.Contains(req.ClassIdentifier(), "PXEClient:Arch:00011") { 143 | resp.Options.Update(dhcpv4.OptBootFileName("ipxe_arm64.efi")) 144 | } else { 145 | resp.Options.Update(dhcpv4.OptBootFileName("ipxe.efi")) 146 | } 147 | //bootFileSize = 1 148 | } else { 149 | resp.Options.Update(dhcpv4.OptBootFileName("undionly.kpxe")) 150 | //bootFileSize = 1 151 | } 152 | } 153 | 154 | if req.IsOptionRequested(dhcpv4.OptionBootFileSize) && bootFileSize > 0 { 155 | resp.Options.Update(dhcpv4.Option{ 156 | Code: dhcpv4.OptionBootFileSize, 157 | Value: Uint8(bootFileSize), 158 | }) 159 | } 160 | 161 | // iPXE specific 162 | if stringSlicesEqual(req.UserClass(), []string{"iPXE"}) { 163 | resp.Options.Update(dhcpv4.Option{ 164 | Code: dhcpv4.GenericOptionCode(176), // ipxe.no-pxedhcp 165 | Value: dhcpv4.Uint16(1), // should be uint8 according to iPXE docs 166 | }) 167 | } 168 | 169 | response: 170 | // continue main handler 171 | if resp != nil { 172 | var peer *net.UDPAddr 173 | if !req.GatewayIPAddr.IsUnspecified() { 174 | // TODO: make RFC8357 compliant 175 | peer = &net.UDPAddr{IP: req.GatewayIPAddr, Port: dhcpv4.ServerPort} 176 | } else if resp.MessageType() == dhcpv4.MessageTypeNak { 177 | peer = &net.UDPAddr{IP: net.IPv4bcast, Port: dhcpv4.ClientPort} 178 | } else if !req.ClientIPAddr.IsUnspecified() { 179 | peer = &net.UDPAddr{IP: req.ClientIPAddr, Port: dhcpv4.ClientPort} 180 | } else if req.IsBroadcast() { 181 | peer = &net.UDPAddr{IP: net.IPv4bcast, Port: dhcpv4.ClientPort} 182 | } else { 183 | // we must inject ARP to unicast to IP/MAC that's not on the network yet 184 | device := server.Interface.Name 185 | if device == "" && oob != nil && oob.IfIndex != 0 { 186 | if netif, err := net.InterfaceByIndex(oob.IfIndex); err == nil { 187 | device = netif.Name 188 | } 189 | } 190 | rawConn, err := server.UdpConn.SyscallConn() 191 | if device != "" && err == nil { 192 | rawConn.Control(func(fd uintptr) { 193 | err = arp.InjectArpFd(fd, resp.YourIPAddr, req.ClientHWAddr, arp.ATF_COM, device) 194 | }) 195 | if err != nil && runtime.GOOS == "linux" { 196 | server.logger.Error(). 197 | Err(err). 198 | Msg("ioctl failed") 199 | } 200 | } 201 | 202 | if device != "" && err == nil { 203 | peer = &net.UDPAddr{IP: resp.YourIPAddr, Port: dhcpv4.ClientPort} 204 | } else { 205 | // fall back to broadcast 206 | peer = &net.UDPAddr{IP: net.IPv4bcast, Port: dhcpv4.ClientPort} 207 | } 208 | } 209 | 210 | var woob *ipv4.ControlMessage 211 | if peer.IP.Equal(net.IPv4bcast) || peer.IP.IsLinkLocalUnicast() { 212 | // Direct broadcasts and link-local to the interface the request was 213 | // received on. Other packets should use the normal routing table in 214 | // case of asymmetric routing. 215 | switch { 216 | case server.Interface.Index != 0: 217 | woob = &ipv4.ControlMessage{IfIndex: server.Interface.Index} 218 | case oob != nil && oob.IfIndex != 0: 219 | woob = &ipv4.ControlMessage{IfIndex: oob.IfIndex} 220 | default: 221 | server.logger.Error(). 222 | Str("peer", peer.String()). 223 | Msg("did not receive interface information") 224 | } 225 | } 226 | 227 | server.logger.Debug(). 228 | Interface("response", resp). 229 | Msg("sending DHCP packet") 230 | 231 | if _, err := server.WriteTo(resp.ToBytes(), woob, peer); err != nil { 232 | server.logger.Error(). 233 | Err(err). 234 | Str("peer", peer.String()). 235 | Msg("conn.Write failed") 236 | } 237 | 238 | } else { 239 | server.logger.Trace(). 240 | Msg("dropping request because response is nil") 241 | } 242 | } 243 | 244 | func stringSlicesEqual(a, b []string) bool { 245 | if len(a) != len(b) { 246 | return false 247 | } 248 | for i, v := range a { 249 | if v != b[i] { 250 | return false 251 | } 252 | } 253 | return true 254 | } 255 | 256 | func getIpv4ForInterface(i int) (net.IP, error) { 257 | netif, err := net.InterfaceByIndex(i) 258 | if err != nil { 259 | return nil, err 260 | } 261 | addresses, err := netif.Addrs() 262 | if err != nil { 263 | return nil, err 264 | } 265 | for _, address := range addresses { 266 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 267 | if ipnet.IP.To4() != nil { 268 | return ipnet.IP.To4(), nil 269 | } 270 | } 271 | } 272 | return nil, errors.New("no IP found") 273 | } 274 | -------------------------------------------------------------------------------- /dhcpd/server.go: -------------------------------------------------------------------------------- 1 | // Package dhcpd contains snippets from MIT-licensed coredhcp project at 2 | // https://github.com/coredhcp/coredhcp 3 | package dhcpd 4 | 5 | import ( 6 | "github.com/DSpeichert/netbootd/store" 7 | "github.com/insomniacslk/dhcp/dhcpv4/server4" 8 | "github.com/rs/zerolog" 9 | "github.com/rs/zerolog/log" 10 | "golang.org/x/net/ipv4" 11 | "net" 12 | ) 13 | 14 | type Server struct { 15 | UdpConn *net.UDPConn 16 | *ipv4.PacketConn 17 | net.Interface 18 | address *net.UDPAddr 19 | logger zerolog.Logger 20 | store *store.Store 21 | } 22 | 23 | func NewServer(addr, ifname string, store *store.Store) (server *Server, err error) { 24 | server = &Server{ 25 | address: &net.UDPAddr{ 26 | IP: net.ParseIP(addr), 27 | Port: 67, 28 | Zone: ifname, 29 | }, 30 | logger: log.With().Str("service", "dhcpv4").Logger(), 31 | store: store, 32 | } 33 | 34 | return server, nil 35 | } 36 | 37 | // MaxDatagram is the maximum length of message that can be received. 38 | const MaxDatagram = 1 << 16 39 | 40 | func (server *Server) Serve() { 41 | var err error 42 | 43 | // binds to specific interface (e.g. eth0) if provided 44 | server.UdpConn, err = server4.NewIPv4UDPConn(server.address.Zone, server.address) 45 | if err != nil { 46 | server.logger.Fatal(). 47 | Err(err). 48 | Msgf("Cannot bind to %+v", server.address) 49 | return 50 | } 51 | server.PacketConn = ipv4.NewPacketConn(server.UdpConn) 52 | var ifi *net.Interface 53 | if server.address.Zone != "" { 54 | ifi, err = net.InterfaceByName(server.address.Zone) 55 | if err != nil { 56 | server.logger.Fatal(). 57 | Err(err). 58 | Msg("could not find interface: " + server.address.Zone) 59 | return 60 | } 61 | server.Interface = *ifi 62 | } else { 63 | // When not bound to an interface, we need the information in each 64 | // packet to know which interface it came on 65 | err = server.SetControlMessage(ipv4.FlagInterface, true) 66 | if err != nil { 67 | server.logger.Fatal(). 68 | Err(err). 69 | Msg("Cannot set control message when not specifying interface") 70 | return 71 | } 72 | } 73 | 74 | if server.address.IP.IsMulticast() { 75 | err = server.JoinGroup(ifi, server.address) 76 | if err != nil { 77 | server.logger.Fatal(). 78 | Err(err). 79 | Msg("Cannot join multicast group") 80 | return 81 | } 82 | } 83 | 84 | log.Debug().Msgf("Listen %s", server.LocalAddr()) 85 | for { 86 | b := make([]byte, MaxDatagram) 87 | 88 | n, oob, peer, err := server.ReadFrom(b) 89 | if err != nil { 90 | server.logger. 91 | Error(). 92 | Err(err). 93 | Msg("error reading from connection") 94 | } 95 | go server.HandleMsg4(b[:n], oob, peer.(*net.UDPAddr)) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /dhcpd/types.go: -------------------------------------------------------------------------------- 1 | package dhcpd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/u-root/uio/uio" 6 | ) 7 | 8 | // Uint8 mirrors dhcpv4.Uint16 9 | type Uint8 uint8 10 | 11 | // ToBytes returns a serialized stream of bytes for this option. 12 | func (o Uint8) ToBytes() []byte { 13 | buf := uio.NewBigEndianBuffer(nil) 14 | buf.Write8(uint8(o)) 15 | return buf.Data() 16 | } 17 | 18 | // String returns a human-readable string for this option. 19 | func (o Uint8) String() string { 20 | return fmt.Sprintf("%d", uint8(o)) 21 | } 22 | 23 | // FromBytes decodes data into o as per RFC 2132, Section 9.10. 24 | func (o *Uint8) FromBytes(data []byte) error { 25 | buf := uio.NewBigEndianBuffer(data) 26 | *o = Uint8(buf.Read8()) 27 | return buf.FinError() 28 | } 29 | -------------------------------------------------------------------------------- /examples/ubuntu-1804.yml: -------------------------------------------------------------------------------- 1 | --- 2 | id: ubuntu-1804 3 | ipv4: 192.168.17.101/24 4 | hostname: ubuntu-machine-1804 5 | domain: test.local 6 | leaseDuration: 1h 7 | 8 | # many values are possible because a single machine may have multiple interfaces 9 | # and it may not be known which one boots first 10 | mac: 11 | - 00:15:5d:bd:be:15 12 | - aa:bb:cc:dd:ee:fc 13 | - aa:bb:cc:dd:ee:fd 14 | - aa:bb:cc:dd:ee:fe 15 | - aa:bb:cc:dd:ee:ff 16 | 17 | # in the "order of preference" 18 | dns: 19 | - 8.8.8.8 20 | - 8.8.4.4 21 | - 2001:4860:4860::8888 22 | - 2001:4860:4860::8844 23 | 24 | # in the "order of preference" 25 | router: 26 | - 192.168.17.1 27 | 28 | # in the "order of preference" 29 | ntp: 30 | - 192.168.17.1 31 | 32 | ipxe: true 33 | bootFilename: install.ipxe 34 | 35 | mounts: 36 | - path: /netboot 37 | pathIsPrefix: true 38 | proxy: http://archive.ubuntu.com/ubuntu/dists/bionic-updates/main/installer-amd64/current/images/hwe-netboot/ubuntu-installer/amd64/ 39 | appendSuffix: true 40 | 41 | - path: /install.ipxe 42 | content: | 43 | #!ipxe 44 | # https://ipxe.org/scripting 45 | 46 | set base {{ .HttpBaseUrl }}/netboot 47 | 48 | {{ $hostnameParts := splitList "." .Manifest.Hostname }} 49 | kernel ${base}/linux gfxpayload=800x600x16,800x600 initrd=initrd.gz auto=true url={{ .HttpBaseUrl.String }}/preseed.txt netcfg/get_ipaddress={{ .Manifest.IPv4.IP }} netcfg/get_netmask={{ .Manifest.IPv4.Netmask }} netcfg/get_gateway={{ first .Manifest.Router }} netcfg/get_nameservers="{{ .Manifest.DNS | join " " }}" netcfg/disable_autoconfig=true hostname={{ first $hostnameParts }} domain={{ rest $hostnameParts | join "." }} DEBCONF_DEBUG=developer 50 | initrd ${base}/initrd.gz 51 | boot 52 | 53 | - path: /ubuntu/ 54 | pathIsPrefix: true 55 | proxy: http://archive.ubuntu.com/ubuntu/ 56 | proxyAppendSuffix: true 57 | 58 | # Note: this example is built for a BIOS (non-UEFI) machine 59 | - path: /preseed.txt 60 | content: | 61 | # do not enable live installer, use normal instead 62 | d-i live-installer/enable boolean false 63 | 64 | ### Localization 65 | # Preseeding only locale sets language, country and locale. 66 | d-i debian-installer/locale string en_US.UTF-8 67 | 68 | # Keyboard selection. 69 | # Disable automatic (interactive) keymap detection. 70 | d-i console-setup/ask_detect boolean false 71 | d-i console-setup/layoutcode string us 72 | d-i keyboard-configuration/xkb-keymap select us 73 | d-i keyboard-configuration/layoutcode string us 74 | # To select a variant of the selected layout: 75 | #d-i keyboard-configuration/xkb-keymap select us(dvorak) 76 | # d-i keyboard-configuration/toggle select No toggling 77 | 78 | ### Network configuration 79 | # Disable network configuration entirely. This is useful for cdrom 80 | # installations on non-networked devices where the network questions, 81 | # warning and long timeouts are a nuisance. 82 | #d-i netcfg/enable boolean false 83 | 84 | # netcfg will choose an interface that has link if possible. This makes it 85 | # skip displaying a list if there is more than one interface. 86 | d-i netcfg/choose_interface select auto 87 | 88 | # To set a different link detection timeout (default is 3 seconds). 89 | # Values are interpreted as seconds. 90 | #d-i netcfg/link_wait_timeout string 10 91 | 92 | # If you have a slow dhcp server and the installer times out waiting for 93 | # it, this might be useful. 94 | #d-i netcfg/dhcp_timeout string 60 95 | #d-i netcfg/dhcpv6_timeout string 60 96 | 97 | # https://askubuntu.com/q/667515 98 | d-i netcfg/disable_dhcp boolean false 99 | 100 | ####### SOME OF THESE SETTINGS HERE DON'T MATTER BECAUSE OF KERNEL BOOT PARAMS OVERRIDE ####### 101 | # 102 | ## If you prefer to configure the network manually, uncomment this line and 103 | ## the static network configuration below. 104 | #d-i netcfg/disable_autoconfig boolean true 105 | # 106 | ## If you want the preconfiguration file to work on systems both with and 107 | ## without a dhcp server, uncomment these lines and the static network 108 | ## configuration below. 109 | ##d-i netcfg/dhcp_failed note 110 | ##d-i netcfg/dhcp_options select Configure network manually 111 | # 112 | ## Static network configuration. 113 | ## 114 | ## IPv4 example 115 | #d-i netcfg/get_ipaddress string {{ .Manifest.IPv4.IP }} 116 | #d-i netcfg/get_netmask string {{ .Manifest.IPv4.Netmask }} 117 | #d-i netcfg/get_gateway string {{ first .Manifest.Router }} 118 | #d-i netcfg/get_nameservers string {{ range $index, $element := .Manifest.DNS}}{{ $element }} {{ end }} 119 | #d-i netcfg/confirm_static boolean true 120 | ## 121 | ## IPv6 example 122 | ##d-i netcfg/get_ipaddress string fc00::2 123 | ##d-i netcfg/get_netmask string ffff:ffff:ffff:ffff:: 124 | ##d-i netcfg/get_gateway string fc00::1 125 | ##d-i netcfg/get_nameservers string fc00::1 126 | ##d-i netcfg/confirm_static boolean true 127 | # 128 | ## Any hostname and domain names assigned from dhcp take precedence over 129 | ## values set here. However, setting the values still prevents the questions 130 | ## from being shown, even if values come from dhcp. 131 | {{ $hostnameParts := splitList "." .Manifest.Hostname }} 132 | d-i netcfg/get_hostname string {{ first $hostnameParts }} 133 | d-i netcfg/get_domain string {{ rest $hostnameParts | join "." }} 134 | # 135 | ## If you want to force a hostname, regardless of what either the DHCP 136 | ## server returns or what the reverse DNS entry for the IP is, uncomment 137 | ## and adjust the following line. 138 | d-i netcfg/hostname string {{ .Manifest.Hostname }} 139 | # 140 | ####### END SETTINGS HERE DON'T MATTER BECAUSE OF KERNEL BOOT PARAMS OVERRIDE ####### 141 | 142 | # Disable that annoying WEP key dialog. 143 | d-i netcfg/wireless_wep string 144 | # The wacky dhcp hostname that some ISPs use as a password of sorts. 145 | #d-i netcfg/dhcp_hostname string radish 146 | 147 | # If non-free firmware is needed for the network or other hardware, you can 148 | # configure the installer to always try to load it, without prompting. Or 149 | # change to false to disable asking. 150 | d-i hw-detect/load_firmware boolean true 151 | 152 | ### Mirror settings 153 | # If you select ftp, the mirror/country string does not need to be set. 154 | #d-i mirror/protocol string ftp 155 | d-i mirror/country string manual 156 | d-i mirror/http/hostname string archive.ubuntu.com 157 | d-i mirror/http/directory string /ubuntu 158 | #d-i mirror/http/proxy string http://proxy:8888 159 | d-i mirror/http/proxy string 160 | 161 | # Alternatively: by default, the installer uses CC.archive.ubuntu.com where 162 | # CC is the ISO-3166-2 code for the selected country. You can preseed this 163 | # so that it does so without asking. 164 | d-i mirror/http/mirror select us.archive.ubuntu.com 165 | 166 | # Suite to install. 167 | d-i mirror/suite string bionic 168 | # Suite to use for loading installer components (optional). 169 | #d-i mirror/udeb/suite string stretch 170 | # Components to use for loading installer components (optional). 171 | #d-i mirror/udeb/components multiselect main, restricted 172 | 173 | ### Account setup 174 | # Skip creation of a root account (normal user account will be able to 175 | # use sudo). The default is false; preseed this to true if you want to set 176 | # a root password. 177 | #d-i passwd/root-login boolean false 178 | # Alternatively, to skip creation of a normal user account. 179 | #d-i passwd/make-user boolean false 180 | 181 | # Root password, either in clear text 182 | d-i passwd/root-password password r00tme 183 | d-i passwd/root-password-again password r00tme 184 | # or encrypted using a crypt(3) hash. 185 | #d-i passwd/root-password-crypted password [crypt(3) hash] 186 | 187 | # To create a normal user account. 188 | d-i passwd/user-fullname string ubuntu 189 | d-i passwd/username string ubuntu 190 | # Normal user's password, either in clear text 191 | d-i passwd/user-password password testinstall 192 | d-i passwd/user-password-again password testinstall 193 | # or encrypted using a crypt(3) hash. 194 | #d-i passwd/user-password-crypted password hashhere 195 | # Create the first user with the specified UID instead of the default. 196 | #d-i passwd/user-uid string 1010 197 | # The installer will warn about weak passwords. If you are sure you know 198 | # what you're doing and want to override it, uncomment this. 199 | d-i user-setup/allow-password-weak boolean true 200 | 201 | # The user account will be added to some standard initial groups. To 202 | # override that, use this. 203 | #d-i passwd/user-default-groups string audio cdrom video 204 | 205 | # Set to true if you want to encrypt the first user's home directory. 206 | d-i user-setup/encrypt-home boolean false 207 | 208 | ### Clock and time zone setup 209 | # Controls whether or not the hardware clock is set to UTC. 210 | d-i clock-setup/utc boolean true 211 | 212 | # You may set this to any valid setting for $TZ; see the contents of 213 | # /usr/share/zoneinfo/ for valid values. 214 | d-i time/zone string UTC 215 | 216 | # Controls whether to use NTP to set the clock during the install 217 | d-i clock-setup/ntp boolean true 218 | # NTP server to use. The default is almost always fine here. 219 | d-i clock-setup/ntp-server string {{ first .Manifest.NTP }} 220 | 221 | ### Partitioning 222 | ## Partitioning example 223 | # If the system has free space you can choose to only partition that space. 224 | # This is only honoured if partman-auto/method (below) is not set. 225 | # Alternatives: custom, some_device, some_device_crypto, some_device_lvm. 226 | #d-i partman-auto/init_automatically_partition select biggest_free 227 | 228 | # Alternatively, you may specify a disk to partition. If the system has only 229 | # one disk the installer will default to using that, but otherwise the device 230 | # name must be given in traditional, non-devfs format (so e.g. /dev/sda 231 | # and not e.g. /dev/discs/disc0/disc). 232 | # For example, to use the first SCSI/SATA hard disk: 233 | d-i partman-auto/disk string /dev/sda 234 | # In addition, you'll need to specify the method to use. 235 | # The presently available methods are: 236 | # - regular: use the usual partition types for your architecture 237 | # - lvm: use LVM to partition the disk 238 | # - crypto: use LVM within an encrypted partition 239 | d-i partman-auto/method string regular 240 | 241 | # If one of the disks that are going to be automatically partitioned 242 | # contains an old LVM configuration, the user will normally receive a 243 | # warning. This can be preseeded away... 244 | d-i partman-lvm/device_remove_lvm boolean true 245 | # https://askubuntu.com/a/1088224 246 | d-i partman-auto/purge_lvm_from_device boolean true 247 | # The same applies to pre-existing software RAID array: 248 | d-i partman-md/device_remove_md boolean true 249 | # And the same goes for the confirmation to write the lvm partitions. 250 | d-i partman-lvm/confirm boolean true 251 | d-i partman-lvm/confirm_nooverwrite boolean true 252 | 253 | # Keep that one set to true so we end up with a UEFI enabled 254 | # system. If set to false, /var/lib/partman/uefi_ignore will be touched 255 | #d-i partman-efi/non_efi_system boolean true 256 | 257 | # disable swap warning 258 | d-i partman-basicfilesystems/no_swap boolean false 259 | 260 | # make it GPT! 261 | #d-i partman-basicfilesystems/choose_label string gpt 262 | #d-i partman-basicfilesystems/default_label string gpt 263 | #d-i partman-partitioning/choose_label string gpt 264 | #d-i partman-partitioning/default_label string gpt 265 | #d-i partman/choose_label string gpt 266 | #d-i partman/default_label string gpt 267 | 268 | # You can choose one of the three predefined partitioning recipes: 269 | # - atomic: all files in one partition 270 | # - home: separate /home partition 271 | # - multi: separate /home, /var, and /tmp partitions 272 | d-i partman-auto/choose_recipe select atomic 273 | 274 | # minimum (MB), priority (higher is higher), max (MB) 275 | d-i partman-auto/expert_recipe string \ 276 | custom :: \ 277 | 512 100 512 fat32 \ 278 | $gptonly{ } \ 279 | $primary{ } label{ esp } \ 280 | method{ efi } format{ } . \ 281 | 1000 100 -1 ext4 \ 282 | $gptonly{ } \ 283 | $primary{ } \ 284 | method{ format } format{ } \ 285 | use_filesystem{ } filesystem{ ext4 } \ 286 | options/relatime{ relatime } \ 287 | options/user_xattr{ user_xattr } \ 288 | options/acl{ acl } \ 289 | mountpoint{ / } label{ root } . 290 | 291 | # https://serverfault.com/a/789339 292 | d-i partman/alignment string optimal 293 | 294 | # If you just want to change the default filesystem from ext3 to something 295 | # else, you can do that without providing a full recipe. 296 | #d-i partman/default_filesystem string ext4 297 | 298 | # The full recipe format is documented in the file partman-auto-recipe.txt 299 | # included in the 'debian-installer' package or available from D-I source 300 | # repository. This also documents how to specify settings such as file 301 | # system labels, volume group names and which physical devices to include 302 | # in a volume group. 303 | 304 | # This makes partman automatically partition without confirmation. 305 | #d-i partman-md/confirm boolean true 306 | d-i partman-partitioning/confirm_write_new_label boolean true 307 | d-i partman/choose_partition select finish 308 | d-i partman/confirm boolean true 309 | d-i partman/confirm_nooverwrite boolean true 310 | 311 | ## Controlling how partitions are mounted 312 | # The default is to mount by UUID, but you can also choose "traditional" to 313 | # use traditional device names, or "label" to try filesystem labels before 314 | # falling back to UUIDs. 315 | d-i partman/mount_style select label 316 | 317 | ### Base system installation 318 | # Configure a path to the preconfigured base filesystem. This can be used to 319 | # specify a path for the installer to retrieve the filesystem image that will 320 | # be deployed to disk and used as a base system for the installation. 321 | #d-i live-installer/net-image string /install/filesystem.squashfs 322 | 323 | # Configure APT to not install recommended packages by default. Use of this 324 | # option can result in an incomplete system and should only be used by very 325 | # experienced users. 326 | #d-i base-installer/install-recommends boolean false 327 | 328 | # The kernel image (meta) package to be installed; "none" can be used if no 329 | # kernel is to be installed. 330 | d-i base-installer/kernel/image string linux-generic-hwe-18.04 331 | 332 | ### Apt setup 333 | # You can choose to install restricted and universe software, or to install 334 | # software from the backports repository. 335 | d-i apt-setup/restricted boolean true 336 | d-i apt-setup/universe boolean true 337 | d-i apt-setup/backports boolean false 338 | # Uncomment this if you don't want to use a network mirror. 339 | #d-i apt-setup/use_mirror boolean false 340 | # Select which update services to use; define the mirrors to be used. 341 | # Values shown below are the normal defaults. 342 | #d-i apt-setup/services-select multiselect security 343 | #d-i apt-setup/security_host string security.ubuntu.com 344 | #d-i apt-setup/security_path string /ubuntu 345 | 346 | # Additional repositories, local[0-9] available 347 | #d-i apt-setup/local0/repository string \ 348 | # http://local.server/ubuntu stretch main 349 | #d-i apt-setup/local0/comment string local server 350 | # Enable deb-src lines 351 | #d-i apt-setup/local0/source boolean true 352 | # URL to the public key of the local repository; you must provide a key or 353 | # apt will complain about the unauthenticated repository and so the 354 | # sources.list line will be left commented out 355 | #d-i apt-setup/local0/key string http://local.server/key 356 | 357 | # By default the installer requires that repositories be authenticated 358 | # using a known gpg key. This setting can be used to disable that 359 | # authentication. Warning: Insecure, not recommended. 360 | #d-i debian-installer/allow_unauthenticated boolean true 361 | 362 | # Uncomment this to add multiarch configuration for i386 363 | #d-i apt-setup/multiarch string i386 364 | 365 | ### Package selection 366 | tasksel tasksel/first multiselect openssh-server 367 | #tasksel tasksel/first multiselect lamp-server, print-server 368 | 369 | # Individual additional packages to install 370 | d-i pkgsel/include string mc htop 371 | 372 | # Whether to upgrade packages after debootstrap. 373 | # Allowed values: none, safe-upgrade, full-upgrade 374 | # d-i pkgsel/upgrade select full-upgrade 375 | 376 | # Language pack selection 377 | #d-i pkgsel/language-packs multiselect de, en, zh 378 | 379 | # Policy for applying updates. May be "none" (no automatic updates), 380 | # "unattended-upgrades" (install security updates automatically), or 381 | # "landscape" (manage system with Landscape). 382 | d-i pkgsel/update-policy select none 383 | 384 | # Some versions of the installer can report back on what software you have 385 | # installed, and what software you use. The default is not to report back, 386 | # but sending reports helps the project determine what software is most 387 | # popular and include it on CDs. 388 | popularity-contest popularity-contest/participate boolean false 389 | 390 | # By default, the system's locate database will be updated after the 391 | # installer has finished installing most packages. This may take a while, so 392 | # if you don't want it, you can set this to "false" to turn it off. 393 | #d-i pkgsel/updatedb boolean true 394 | 395 | ### Boot loader installation 396 | # Grub is the default boot loader (for x86). If you want lilo installed 397 | # instead, uncomment this: 398 | #d-i grub-installer/skip boolean true 399 | # To also skip installing lilo, and install no bootloader, uncomment this 400 | # too: 401 | #d-i lilo-installer/skip boolean true 402 | 403 | # This is fairly safe to set, it makes grub install automatically to the MBR 404 | # if no other operating system is detected on the machine. 405 | d-i grub-installer/only_debian boolean true 406 | 407 | # This one makes grub-installer install to the MBR if it also finds some other 408 | # OS, which is less safe as it might not be able to boot that other OS. 409 | d-i grub-installer/with_other_os boolean true 410 | 411 | # Due notably to potential USB sticks, the location of the MBR can not be 412 | # determined safely in general, so this needs to be specified. 413 | # To install to the first device (assuming it is not a USB stick): 414 | d-i grub-installer/bootdev string default 415 | 416 | # Alternatively, if you want to install to a location other than the mbr, 417 | # uncomment and edit these lines: 418 | #d-i grub-installer/only_debian boolean false 419 | #d-i grub-installer/with_other_os boolean false 420 | #d-i grub-installer/bootdev string (hd0,1) 421 | # To install grub to multiple disks: 422 | #d-i grub-installer/bootdev string (hd0,1) (hd1,1) (hd2,1) 423 | 424 | # Optional password for grub, either in clear text 425 | #d-i grub-installer/password password r00tme 426 | #d-i grub-installer/password-again password r00tme 427 | # or encrypted using an MD5 hash, see grub-md5-crypt(8). 428 | #d-i grub-installer/password-crypted password [MD5 hash] 429 | 430 | # Use the following option to add additional boot parameters for the 431 | # installed system (if supported by the bootloader installer). 432 | # Note: options passed to the installer will be added automatically. 433 | d-i debian-installer/add-kernel-opts string consoleblank=0 434 | 435 | ### Finishing up the installation 436 | # During installations from serial console, the regular virtual consoles 437 | # (VT1-VT6) are normally disabled in /etc/inittab. Uncomment the next 438 | # line to prevent this. 439 | #d-i finish-install/keep-consoles boolean true 440 | 441 | # Avoid that last message about the install being complete. 442 | d-i finish-install/reboot_in_progress note 443 | 444 | # This will prevent the installer from ejecting the CD during the reboot, 445 | # which is useful in some situations. 446 | d-i cdrom-detect/eject boolean false 447 | 448 | # This is how to make the installer shutdown when finished, but not 449 | # reboot into the installed system. 450 | #d-i debian-installer/exit/halt boolean true 451 | # This will power off the machine instead of just halting it. 452 | #d-i debian-installer/exit/poweroff boolean true 453 | 454 | ### Preseeding other packages 455 | # Depending on what software you choose to install, or if things go wrong 456 | # during the installation process, it's possible that other questions may 457 | # be asked. You can preseed those too, of course. To get a list of every 458 | # possible question that could be asked during an install, do an 459 | # installation, and then run these commands: 460 | # debconf-get-selections --installer > file 461 | # debconf-get-selections >> file 462 | 463 | #### Advanced options 464 | ### Running custom commands during the installation 465 | ## i386 Preseed Example 466 | # d-i preseeding is inherently not secure. Nothing in the installer checks 467 | # for attempts at buffer overflows or other exploits of the values of a 468 | # preconfiguration file like this one. Only use preconfiguration files from 469 | # trusted locations! To drive that home, and because it's generally useful, 470 | # here's a way to run any shell command you'd like inside the installer, 471 | # automatically. 472 | 473 | # This first command is run as early as possible, just after 474 | # preseeding is read. 475 | # d-i preseed/early_command command here 476 | 477 | # This command is run immediately before the partitioner starts. It may be 478 | # useful to apply dynamic partitioner preseeding that depends on the state 479 | # of the disks (which may not be visible when preseed/early_command runs). 480 | #d-i partman/early_command \ 481 | # string debconf-set partman-auto/disk "$(list-devices disk | head -n1)" 482 | 483 | # This command is run just before the install finishes, but when there is 484 | # still a usable /target directory. You can chroot to /target and use it 485 | # directly, or use the apt-install and in-target commands to easily install 486 | # packages and run commands in the target system. 487 | #d-i preseed/late_command string apt-install zsh; in-target chsh -s /bin/zsh 488 | 489 | # https://bugs.launchpad.net/maas/+bug/1302158 490 | d-i anna/no_kernel_modules boolean true 491 | 492 | # https://github.com/andrewdmcleod/preseed-tokenise/blob/master/preseed.cfg.template 493 | kexec-tools kexec-tools/load_kexec boolean true 494 | kexec-tools kexec-tools/use_grub_config boolean true 495 | -------------------------------------------------------------------------------- /examples/ubuntu-2004-ram.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This example manifest boots Ubuntu 20.04 into ram using tmpfs-mounted root filesystem downloaded over HTTP 3 | id: ubuntu-2004-ram 4 | ipv4: 192.168.17.103/24 5 | hostname: ubuntu-2004-ram 6 | domain: test.local 7 | leaseDuration: 1h 8 | 9 | # many values are possible because a single machine may have multiple interfaces 10 | # and it may not be known which one boots first 11 | mac: 12 | - 00:15:5d:bd:be:13 13 | 14 | # in the "order of preference" 15 | dns: 16 | - 8.8.8.8 17 | - 8.8.4.4 18 | - 2001:4860:4860::8888 19 | - 2001:4860:4860::8844 20 | 21 | # in the "order of preference" 22 | router: 23 | - 192.168.17.1 24 | 25 | # in the "order of preference" 26 | ntp: 27 | - 192.168.17.1 28 | 29 | ipxe: true 30 | bootFilename: install.ipxe 31 | 32 | mounts: 33 | - path: /kernel 34 | proxy: https://cloud-images.ubuntu.com/focal/current/unpacked/focal-server-cloudimg-amd64-vmlinuz-generic 35 | 36 | # Note: There is no readily available initrd that has the necessary initramfs modules. 37 | # Build one by installing packages: live-boot cloud-initramfs-rooturl 38 | # http://manpages.ubuntu.com/manpages/focal/man7/live-boot.7.html 39 | - path: /initrd 40 | proxy: https://path.to.initrd... 41 | 42 | - path: /root.tar.xz 43 | proxy: https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-amd64-root.tar.xz 44 | 45 | - path: /install.ipxe 46 | content: | 47 | #!ipxe 48 | # https://ipxe.org/scripting 49 | kernel kernel initrd=initrd root={{ .HttpBaseUrl }}/root.tar.xz ip=dhcp ds=nocloud-net;s={{ .HttpBaseUrl }}/ network-config=disabled 50 | initrd initrd 51 | boot 52 | 53 | - path: /meta-data 54 | content: | 55 | # empty 56 | 57 | - path: /user-data 58 | content: | 59 | #cloud-config 60 | preserve_sources_list: true 61 | password: ubuntu 62 | ssh_pwauth: yes 63 | chpasswd: 64 | expire: false 65 | 66 | - path: /vendor-data 67 | content: | 68 | # empty -------------------------------------------------------------------------------- /examples/ubuntu-2004.yml: -------------------------------------------------------------------------------- 1 | --- 2 | id: ubuntu-2004 3 | ipv4: 192.168.17.102/24 4 | hostname: ubuntu-machine-2004 5 | domain: test.local 6 | leaseDuration: 1h 7 | 8 | # many values are possible because a single machine may have multiple interfaces 9 | # and it may not be known which one boots first 10 | mac: 11 | - 00:15:5d:bd:be:16 12 | 13 | # in the "order of preference" 14 | dns: 15 | - 8.8.8.8 16 | - 8.8.4.4 17 | - 2001:4860:4860::8888 18 | - 2001:4860:4860::8844 19 | 20 | # in the "order of preference" 21 | router: 22 | - 192.168.17.1 23 | 24 | # in the "order of preference" 25 | ntp: 26 | - 192.168.17.1 27 | 28 | ipxe: true 29 | bootFilename: install.ipxe 30 | 31 | mounts: 32 | - path: / 33 | pathIsPrefix: true 34 | proxy: http://archive.ubuntu.com/ubuntu/dists/focal-updates/main/installer-amd64/current/legacy-images/netboot/ubuntu-installer/amd64/ 35 | appendSuffix: true 36 | 37 | - path: /install.ipxe 38 | content: | 39 | #!ipxe 40 | # https://ipxe.org/scripting 41 | 42 | {{ $hostnameParts := splitList "." .Manifest.Hostname }} 43 | kernel {{ .HttpBaseUrl }}/linux initrd=initrd.gz net.ifnames=0 biosdevname=0 ip=dhcp autoinstall auto=true url={{ .HttpBaseUrl }}/preseed.txt DEBCONF_DEBUG=developer hostname={{ first $hostnameParts }} domain={{ rest $hostnameParts | join "." }} 44 | initrd {{ .HttpBaseUrl }}/initrd.gz 45 | boot 46 | 47 | # Note: this example is built for a UEFI machine 48 | - path: /preseed.txt 49 | content: | 50 | # do not enable live installer, use normal instead 51 | d-i live-installer/enable boolean false 52 | 53 | ### Localization 54 | # Preseeding only locale sets language, country and locale. 55 | d-i debian-installer/locale string en_US.UTF-8 56 | 57 | # Keyboard selection. 58 | # Disable automatic (interactive) keymap detection. 59 | d-i console-setup/ask_detect boolean false 60 | d-i console-setup/layoutcode string us 61 | d-i keyboard-configuration/xkb-keymap select us 62 | d-i keyboard-configuration/layoutcode string us 63 | # To select a variant of the selected layout: 64 | #d-i keyboard-configuration/xkb-keymap select us(dvorak) 65 | # d-i keyboard-configuration/toggle select No toggling 66 | 67 | ### Network configuration 68 | # Disable network configuration entirely. This is useful for cdrom 69 | # installations on non-networked devices where the network questions, 70 | # warning and long timeouts are a nuisance. 71 | #d-i netcfg/enable boolean false 72 | 73 | # netcfg will choose an interface that has link if possible. This makes it 74 | # skip displaying a list if there is more than one interface. 75 | d-i netcfg/choose_interface select auto 76 | 77 | # To set a different link detection timeout (default is 3 seconds). 78 | # Values are interpreted as seconds. 79 | #d-i netcfg/link_wait_timeout string 10 80 | 81 | # If you have a slow dhcp server and the installer times out waiting for 82 | # it, this might be useful. 83 | #d-i netcfg/dhcp_timeout string 60 84 | #d-i netcfg/dhcpv6_timeout string 60 85 | 86 | # https://askubuntu.com/q/667515 87 | d-i netcfg/disable_dhcp boolean false 88 | 89 | ####### SOME OF THESE SETTINGS HERE DON'T MATTER BECAUSE OF KERNEL BOOT PARAMS OVERRIDE ####### 90 | # 91 | ## If you prefer to configure the network manually, uncomment this line and 92 | ## the static network configuration below. 93 | #d-i netcfg/disable_autoconfig boolean true 94 | # 95 | ## If you want the preconfiguration file to work on systems both with and 96 | ## without a dhcp server, uncomment these lines and the static network 97 | ## configuration below. 98 | ##d-i netcfg/dhcp_failed note 99 | ##d-i netcfg/dhcp_options select Configure network manually 100 | # 101 | ## Static network configuration. 102 | ## 103 | ## IPv4 example 104 | #d-i netcfg/get_ipaddress string {{ .Manifest.IPv4.IP }} 105 | #d-i netcfg/get_netmask string {{ .Manifest.IPv4.Netmask }} 106 | #d-i netcfg/get_gateway string {{ first .Manifest.Router }} 107 | #d-i netcfg/get_nameservers string {{ range $index, $element := .Manifest.DNS}}{{ $element }} {{ end }} 108 | #d-i netcfg/confirm_static boolean true 109 | ## 110 | ## IPv6 example 111 | ##d-i netcfg/get_ipaddress string fc00::2 112 | ##d-i netcfg/get_netmask string ffff:ffff:ffff:ffff:: 113 | ##d-i netcfg/get_gateway string fc00::1 114 | ##d-i netcfg/get_nameservers string fc00::1 115 | ##d-i netcfg/confirm_static boolean true 116 | # 117 | ## Any hostname and domain names assigned from dhcp take precedence over 118 | ## values set here. However, setting the values still prevents the questions 119 | ## from being shown, even if values come from dhcp. 120 | {{ $hostnameParts := splitList "." .Manifest.Hostname }} 121 | d-i netcfg/get_hostname string {{ first $hostnameParts }} 122 | d-i netcfg/get_domain string {{ rest $hostnameParts | join "." }} 123 | # 124 | ## If you want to force a hostname, regardless of what either the DHCP 125 | ## server returns or what the reverse DNS entry for the IP is, uncomment 126 | ## and adjust the following line. 127 | d-i netcfg/hostname string {{ .Manifest.Hostname }} 128 | # 129 | ####### END SETTINGS HERE DON'T MATTER BECAUSE OF KERNEL BOOT PARAMS OVERRIDE ####### 130 | 131 | # Disable that annoying WEP key dialog. 132 | d-i netcfg/wireless_wep string 133 | # The wacky dhcp hostname that some ISPs use as a password of sorts. 134 | #d-i netcfg/dhcp_hostname string radish 135 | 136 | # If non-free firmware is needed for the network or other hardware, you can 137 | # configure the installer to always try to load it, without prompting. Or 138 | # change to false to disable asking. 139 | d-i hw-detect/load_firmware boolean true 140 | 141 | ### Mirror settings 142 | # If you select ftp, the mirror/country string does not need to be set. 143 | #d-i mirror/protocol string ftp 144 | d-i mirror/country string manual 145 | d-i mirror/http/hostname string archive.ubuntu.com 146 | d-i mirror/http/directory string /ubuntu 147 | #d-i mirror/http/proxy string http://proxy:8888 148 | d-i mirror/http/proxy string 149 | 150 | # Alternatively: by default, the installer uses CC.archive.ubuntu.com where 151 | # CC is the ISO-3166-2 code for the selected country. You can preseed this 152 | # so that it does so without asking. 153 | d-i mirror/http/mirror select us.archive.ubuntu.com 154 | 155 | # Suite to install. 156 | d-i mirror/suite string focal 157 | # Suite to use for loading installer components (optional). 158 | #d-i mirror/udeb/suite string stretch 159 | # Components to use for loading installer components (optional). 160 | #d-i mirror/udeb/components multiselect main, restricted 161 | 162 | ### Account setup 163 | # Skip creation of a root account (normal user account will be able to 164 | # use sudo). The default is false; preseed this to true if you want to set 165 | # a root password. 166 | #d-i passwd/root-login boolean false 167 | # Alternatively, to skip creation of a normal user account. 168 | #d-i passwd/make-user boolean false 169 | 170 | # Root password, either in clear text 171 | #d-i passwd/root-password password r00tme 172 | #d-i passwd/root-password-again password r00tme 173 | # or encrypted using a crypt(3) hash. 174 | #d-i passwd/root-password-crypted password [crypt(3) hash] 175 | 176 | # To create a normal user account. 177 | d-i passwd/user-fullname string ubuntu 178 | d-i passwd/username string ubuntu 179 | # Normal user's password, either in clear text 180 | d-i passwd/user-password password testinstall 181 | d-i passwd/user-password-again password testinstall 182 | # or encrypted using a crypt(3) hash. 183 | #d-i passwd/user-password-crypted password hashhere 184 | # Create the first user with the specified UID instead of the default. 185 | #d-i passwd/user-uid string 1010 186 | # The installer will warn about weak passwords. If you are sure you know 187 | # what you're doing and want to override it, uncomment this. 188 | d-i user-setup/allow-password-weak boolean true 189 | 190 | # The user account will be added to some standard initial groups. To 191 | # override that, use this. 192 | #d-i passwd/user-default-groups string audio cdrom video 193 | 194 | # Set to true if you want to encrypt the first user's home directory. 195 | d-i user-setup/encrypt-home boolean false 196 | 197 | ### Clock and time zone setup 198 | # Controls whether or not the hardware clock is set to UTC. 199 | d-i clock-setup/utc boolean true 200 | 201 | # You may set this to any valid setting for $TZ; see the contents of 202 | # /usr/share/zoneinfo/ for valid values. 203 | d-i time/zone string UTC 204 | 205 | # Controls whether to use NTP to set the clock during the install 206 | d-i clock-setup/ntp boolean true 207 | # NTP server to use. The default is almost always fine here. 208 | d-i clock-setup/ntp-server string {{ first .Manifest.NTP }} 209 | 210 | ### Partitioning 211 | ## Partitioning example 212 | # If the system has free space you can choose to only partition that space. 213 | # This is only honoured if partman-auto/method (below) is not set. 214 | # Alternatives: custom, some_device, some_device_crypto, some_device_lvm. 215 | #d-i partman-auto/init_automatically_partition select biggest_free 216 | 217 | # Alternatively, you may specify a disk to partition. If the system has only 218 | # one disk the installer will default to using that, but otherwise the device 219 | # name must be given in traditional, non-devfs format (so e.g. /dev/sda 220 | # and not e.g. /dev/discs/disc0/disc). 221 | # For example, to use the first SCSI/SATA hard disk: 222 | d-i partman-auto/disk string /dev/sda 223 | # In addition, you'll need to specify the method to use. 224 | # The presently available methods are: 225 | # - regular: use the usual partition types for your architecture 226 | # - lvm: use LVM to partition the disk 227 | # - crypto: use LVM within an encrypted partition 228 | d-i partman-auto/method string regular 229 | 230 | # If one of the disks that are going to be automatically partitioned 231 | # contains an old LVM configuration, the user will normally receive a 232 | # warning. This can be preseeded away... 233 | d-i partman-lvm/device_remove_lvm boolean true 234 | # https://askubuntu.com/a/1088224 235 | d-i partman-auto/purge_lvm_from_device boolean true 236 | # The same applies to pre-existing software RAID array: 237 | d-i partman-md/device_remove_md boolean true 238 | # And the same goes for the confirmation to write the lvm partitions. 239 | d-i partman-lvm/confirm boolean true 240 | d-i partman-lvm/confirm_nooverwrite boolean true 241 | 242 | # Keep that one set to true so we end up with a UEFI enabled 243 | # system. If set to false, /var/lib/partman/uefi_ignore will be touched 244 | d-i partman-efi/non_efi_system boolean true 245 | 246 | # disable swap warning 247 | d-i partman-basicfilesystems/no_swap boolean false 248 | 249 | # make it GPT! 250 | d-i partman-basicfilesystems/choose_label string gpt 251 | d-i partman-basicfilesystems/default_label string gpt 252 | d-i partman-partitioning/choose_label string gpt 253 | d-i partman-partitioning/default_label string gpt 254 | d-i partman/choose_label string gpt 255 | d-i partman/default_label string gpt 256 | 257 | # You can choose one of the three predefined partitioning recipes: 258 | # - atomic: all files in one partition 259 | # - home: separate /home partition 260 | # - multi: separate /home, /var, and /tmp partitions 261 | d-i partman-auto/choose_recipe select custom 262 | 263 | # minimum (MB), priority (higher is higher), max (MB) 264 | d-i partman-auto/expert_recipe string \ 265 | custom :: \ 266 | 512 200 512 fat32 \ 267 | $gptonly{ } \ 268 | $primary{ } label{ esp } \ 269 | method{ efi } format{ } . \ 270 | 1000 20000 -1 ext4 \ 271 | $gptonly{ } \ 272 | $primary{ } \ 273 | method{ format } format{ } \ 274 | use_filesystem{ } filesystem{ ext4 } \ 275 | options/relatime{ relatime } \ 276 | options/user_xattr{ user_xattr } \ 277 | options/acl{ acl } \ 278 | mountpoint{ / } label{ root } . 279 | 280 | # https://serverfault.com/a/789339 281 | d-i partman/alignment string optimal 282 | 283 | # If you just want to change the default filesystem from ext3 to something 284 | # else, you can do that without providing a full recipe. 285 | #d-i partman/default_filesystem string ext4 286 | 287 | # The full recipe format is documented in the file partman-auto-recipe.txt 288 | # included in the 'debian-installer' package or available from D-I source 289 | # repository. This also documents how to specify settings such as file 290 | # system labels, volume group names and which physical devices to include 291 | # in a volume group. 292 | 293 | # This makes partman automatically partition without confirmation. 294 | d-i partman-md/confirm boolean true 295 | d-i partman-partitioning/confirm_write_new_label boolean true 296 | d-i partman/choose_partition select finish 297 | d-i partman/confirm boolean true 298 | d-i partman/confirm_nooverwrite boolean true 299 | 300 | ## Controlling how partitions are mounted 301 | # The default is to mount by UUID, but you can also choose "traditional" to 302 | # use traditional device names, or "label" to try filesystem labels before 303 | # falling back to UUIDs. 304 | d-i partman/mount_style select label 305 | 306 | ### Base system installation 307 | # Configure a path to the preconfigured base filesystem. This can be used to 308 | # specify a path for the installer to retrieve the filesystem image that will 309 | # be deployed to disk and used as a base system for the installation. 310 | #d-i live-installer/net-image string /install/filesystem.squashfs 311 | 312 | # Configure APT to not install recommended packages by default. Use of this 313 | # option can result in an incomplete system and should only be used by very 314 | # experienced users. 315 | #d-i base-installer/install-recommends boolean false 316 | 317 | # The kernel image (meta) package to be installed; "none" can be used if no 318 | # kernel is to be installed. 319 | d-i base-installer/kernel/image string linux-generic-hwe-20.04 320 | 321 | ### Apt setup 322 | # You can choose to install restricted and universe software, or to install 323 | # software from the backports repository. 324 | d-i apt-setup/restricted boolean true 325 | d-i apt-setup/universe boolean true 326 | d-i apt-setup/backports boolean false 327 | # Uncomment this if you don't want to use a network mirror. 328 | #d-i apt-setup/use_mirror boolean false 329 | # Select which update services to use; define the mirrors to be used. 330 | # Values shown below are the normal defaults. 331 | #d-i apt-setup/services-select multiselect security 332 | #d-i apt-setup/security_host string security.ubuntu.com 333 | #d-i apt-setup/security_path string /ubuntu 334 | 335 | # Additional repositories, local[0-9] available 336 | #d-i apt-setup/local0/repository string \ 337 | # http://local.server/ubuntu stretch main 338 | #d-i apt-setup/local0/comment string local server 339 | # Enable deb-src lines 340 | #d-i apt-setup/local0/source boolean true 341 | # URL to the public key of the local repository; you must provide a key or 342 | # apt will complain about the unauthenticated repository and so the 343 | # sources.list line will be left commented out 344 | #d-i apt-setup/local0/key string http://local.server/key 345 | 346 | # By default the installer requires that repositories be authenticated 347 | # using a known gpg key. This setting can be used to disable that 348 | # authentication. Warning: Insecure, not recommended. 349 | #d-i debian-installer/allow_unauthenticated boolean true 350 | 351 | # Uncomment this to add multiarch configuration for i386 352 | #d-i apt-setup/multiarch string i386 353 | 354 | ### Package selection 355 | tasksel tasksel/first multiselect openssh-server 356 | #tasksel tasksel/first multiselect lamp-server, print-server 357 | 358 | # Individual additional packages to install 359 | d-i pkgsel/include string mc htop 360 | 361 | # Whether to upgrade packages after debootstrap. 362 | # Allowed values: none, safe-upgrade, full-upgrade 363 | # d-i pkgsel/upgrade select full-upgrade 364 | 365 | # Language pack selection 366 | #d-i pkgsel/language-packs multiselect de, en, zh 367 | 368 | # Policy for applying updates. May be "none" (no automatic updates), 369 | # "unattended-upgrades" (install security updates automatically), or 370 | # "landscape" (manage system with Landscape). 371 | d-i pkgsel/update-policy select none 372 | 373 | # Some versions of the installer can report back on what software you have 374 | # installed, and what software you use. The default is not to report back, 375 | # but sending reports helps the project determine what software is most 376 | # popular and include it on CDs. 377 | popularity-contest popularity-contest/participate boolean false 378 | 379 | # By default, the system's locate database will be updated after the 380 | # installer has finished installing most packages. This may take a while, so 381 | # if you don't want it, you can set this to "false" to turn it off. 382 | #d-i pkgsel/updatedb boolean true 383 | 384 | ### Boot loader installation 385 | # Grub is the default boot loader (for x86). If you want lilo installed 386 | # instead, uncomment this: 387 | #d-i grub-installer/skip boolean true 388 | # To also skip installing lilo, and install no bootloader, uncomment this 389 | # too: 390 | #d-i lilo-installer/skip boolean true 391 | 392 | # This is fairly safe to set, it makes grub install automatically to the MBR 393 | # if no other operating system is detected on the machine. 394 | d-i grub-installer/only_debian boolean true 395 | 396 | # This one makes grub-installer install to the MBR if it also finds some other 397 | # OS, which is less safe as it might not be able to boot that other OS. 398 | d-i grub-installer/with_other_os boolean true 399 | 400 | # Due notably to potential USB sticks, the location of the MBR can not be 401 | # determined safely in general, so this needs to be specified. 402 | # To install to the first device (assuming it is not a USB stick): 403 | d-i grub-installer/bootdev string default 404 | 405 | # Alternatively, if you want to install to a location other than the mbr, 406 | # uncomment and edit these lines: 407 | #d-i grub-installer/only_debian boolean false 408 | #d-i grub-installer/with_other_os boolean false 409 | #d-i grub-installer/bootdev string (hd0,1) 410 | # To install grub to multiple disks: 411 | #d-i grub-installer/bootdev string (hd0,1) (hd1,1) (hd2,1) 412 | 413 | # Optional password for grub, either in clear text 414 | #d-i grub-installer/password password r00tme 415 | #d-i grub-installer/password-again password r00tme 416 | # or encrypted using an MD5 hash, see grub-md5-crypt(8). 417 | #d-i grub-installer/password-crypted password [MD5 hash] 418 | 419 | # Use the following option to add additional boot parameters for the 420 | # installed system (if supported by the bootloader installer). 421 | # Note: options passed to the installer will be added automatically. 422 | d-i debian-installer/add-kernel-opts string consoleblank=0 423 | 424 | ### Finishing up the installation 425 | # During installations from serial console, the regular virtual consoles 426 | # (VT1-VT6) are normally disabled in /etc/inittab. Uncomment the next 427 | # line to prevent this. 428 | #d-i finish-install/keep-consoles boolean true 429 | 430 | # Avoid that last message about the install being complete. 431 | d-i finish-install/reboot_in_progress note 432 | 433 | # This will prevent the installer from ejecting the CD during the reboot, 434 | # which is useful in some situations. 435 | d-i cdrom-detect/eject boolean false 436 | 437 | # This is how to make the installer shutdown when finished, but not 438 | # reboot into the installed system. 439 | #d-i debian-installer/exit/halt boolean true 440 | # This will power off the machine instead of just halting it. 441 | #d-i debian-installer/exit/poweroff boolean true 442 | 443 | ### Preseeding other packages 444 | # Depending on what software you choose to install, or if things go wrong 445 | # during the installation process, it's possible that other questions may 446 | # be asked. You can preseed those too, of course. To get a list of every 447 | # possible question that could be asked during an install, do an 448 | # installation, and then run these commands: 449 | # debconf-get-selections --installer > file 450 | # debconf-get-selections >> file 451 | 452 | #### Advanced options 453 | ### Running custom commands during the installation 454 | ## i386 Preseed Example 455 | # d-i preseeding is inherently not secure. Nothing in the installer checks 456 | # for attempts at buffer overflows or other exploits of the values of a 457 | # preconfiguration file like this one. Only use preconfiguration files from 458 | # trusted locations! To drive that home, and because it's generally useful, 459 | # here's a way to run any shell command you'd like inside the installer, 460 | # automatically. 461 | 462 | # This first command is run as early as possible, just after 463 | # preseeding is read. 464 | # d-i preseed/early_command command here 465 | 466 | # This command is run immediately before the partitioner starts. It may be 467 | # useful to apply dynamic partitioner preseeding that depends on the state 468 | # of the disks (which may not be visible when preseed/early_command runs). 469 | #d-i partman/early_command \ 470 | # string debconf-set partman-auto/disk "$(list-devices disk | head -n1)" 471 | 472 | # This command is run just before the install finishes, but when there is 473 | # still a usable /target directory. You can chroot to /target and use it 474 | # directly, or use the apt-install and in-target commands to easily install 475 | # packages and run commands in the target system. 476 | #d-i preseed/late_command string apt-install zsh; in-target chsh -s /bin/zsh 477 | 478 | # https://bugs.launchpad.net/maas/+bug/1302158 479 | d-i anna/no_kernel_modules boolean true 480 | 481 | # https://github.com/andrewdmcleod/preseed-tokenise/blob/master/preseed.cfg.template 482 | kexec-tools kexec-tools/load_kexec boolean true 483 | kexec-tools kexec-tools/use_grub_config boolean true 484 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/DSpeichert/netbootd 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/Masterminds/goutils v1.1.1 // indirect 7 | github.com/Masterminds/semver v1.5.0 // indirect 8 | github.com/Masterminds/sprig v2.22.0+incompatible 9 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf 10 | github.com/fsnotify/fsnotify v1.4.9 11 | github.com/google/uuid v1.2.0 // indirect 12 | github.com/gorilla/mux v1.8.0 13 | github.com/huandu/xstrings v1.3.2 // indirect 14 | github.com/imdario/mergo v0.3.12 // indirect 15 | github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e 16 | github.com/mitchellh/copystructure v1.2.0 // indirect 17 | github.com/pin/tftp v2.1.0+incompatible 18 | github.com/rs/zerolog v1.23.0 19 | github.com/spf13/cobra v1.2.1 20 | github.com/spf13/viper v1.8.1 21 | github.com/stretchr/objx v0.1.1 // indirect 22 | github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7 23 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect 24 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e 25 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c 26 | gopkg.in/yaml.v2 v2.4.0 27 | ) 28 | 29 | replace github.com/pin/tftp => github.com/digitalrebar/tftp v0.0.0-20200914190809-39d58dc90c67 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 17 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 18 | cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= 19 | cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= 20 | cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= 21 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 22 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 23 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 24 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 25 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 26 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 27 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 28 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 29 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 30 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 31 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 32 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 33 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 34 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 35 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 36 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 37 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 38 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 39 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 40 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 41 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 42 | github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= 43 | github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= 44 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 45 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 46 | github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= 47 | github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 48 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 49 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 50 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 51 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 52 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 53 | github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= 54 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 55 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 56 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 57 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 58 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 59 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 60 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 61 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 62 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 63 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= 64 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 65 | github.com/coreos/go-systemd/v22 v22.3.2 h1:D9/bQk5vlXQFZ6Kwuu6zaiXJ9oTPe68++AzAJc1DzSI= 66 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 67 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 68 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 69 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 70 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 71 | github.com/digitalrebar/tftp v0.0.0-20200914190809-39d58dc90c67 h1:ijr3xhGZWNr0exOpJza5U9/3ikCR9+/71lZqME1HEl0= 72 | github.com/digitalrebar/tftp v0.0.0-20200914190809-39d58dc90c67/go.mod h1:1kbtV8n0I3ujA7FewmPymdTyq7Lgk5UhmEy7pcrVVWU= 73 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 74 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 75 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 76 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 77 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 78 | github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 79 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 80 | github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= 81 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 82 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 83 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 84 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 85 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 86 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 87 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 88 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 89 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 90 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 91 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 92 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 93 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 94 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 95 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 96 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 97 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 98 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 99 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 100 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 101 | github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 102 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 103 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 104 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 105 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 106 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 107 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 108 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 109 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 110 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 111 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 112 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 113 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 114 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 115 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 116 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 117 | github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= 118 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 119 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 120 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 121 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 122 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 123 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 124 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 125 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 126 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 127 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 128 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 129 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 130 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 131 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 132 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 133 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 134 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 135 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 136 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 137 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 138 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 139 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 140 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 141 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 142 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 143 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 144 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 145 | github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 146 | github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 147 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 148 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 149 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= 150 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 151 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 152 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 153 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 154 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 155 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 156 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 157 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 158 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 159 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 160 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 161 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 162 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 163 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 164 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 165 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 166 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 167 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 168 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 169 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 170 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 171 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 172 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 173 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 174 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 175 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 176 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 177 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 178 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 179 | github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= 180 | github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 181 | github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= 182 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 183 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 184 | github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= 185 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 186 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 187 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 188 | github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e h1:sgh63o+pm5kcdrgyYaCIoeD7mccyL6MscVmy+DvY6C4= 189 | github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E= 190 | github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= 191 | github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= 192 | github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= 193 | github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= 194 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 195 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 196 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 197 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 198 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 199 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 200 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 201 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 202 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 203 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 204 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 205 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 206 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 207 | github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= 208 | github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 209 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 210 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 211 | github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE= 212 | github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= 213 | github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= 214 | github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= 215 | github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= 216 | github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= 217 | github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= 218 | github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w= 219 | github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= 220 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 221 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 222 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 223 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 224 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 225 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 226 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 227 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 228 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 229 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 230 | github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= 231 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 232 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 233 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 234 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 235 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 236 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 237 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 238 | github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= 239 | github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 240 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 241 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 242 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 243 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 244 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 245 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 246 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 247 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 248 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 249 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 250 | github.com/rs/zerolog v1.23.0 h1:UskrK+saS9P9Y789yNNulYKdARjPZuS35B8gJF2x60g= 251 | github.com/rs/zerolog v1.23.0/go.mod h1:6c7hFfxPOy7TacJc4Fcdi24/J0NKYGzjG8FWRI916Qo= 252 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 253 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 254 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 255 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 256 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 257 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 258 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 259 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 260 | github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= 261 | github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= 262 | github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= 263 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 264 | github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= 265 | github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= 266 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 267 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 268 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 269 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 270 | github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= 271 | github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= 272 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 273 | github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= 274 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 275 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 276 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 277 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 278 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 279 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 280 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 281 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 282 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 283 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 284 | github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= 285 | github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7 h1:XMAtQHwKjWHIRwg+8Nj/rzUomQY1q6cM3ncA0wP8GU4= 286 | github.com/u-root/uio v0.0.0-20210528151154-e40b768296a7/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA= 287 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 288 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 289 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 290 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 291 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 292 | go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= 293 | go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= 294 | go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= 295 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 296 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 297 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 298 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 299 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 300 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 301 | go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= 302 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 303 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 304 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= 305 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 306 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 307 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 308 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 309 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 310 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 311 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 312 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= 313 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 314 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 315 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 316 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 317 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 318 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 319 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 320 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 321 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 322 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 323 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 324 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 325 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 326 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 327 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 328 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 329 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 330 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 331 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 332 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 333 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 334 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 335 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 336 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 337 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 338 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 339 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 340 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 341 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 342 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 343 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 344 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 345 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 346 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 347 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 348 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 349 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 350 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 351 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 352 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 353 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 354 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 355 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 356 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 357 | golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 358 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 359 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 360 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 361 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 362 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 363 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 364 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 365 | golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 366 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 367 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 368 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 369 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 370 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 371 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 372 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 373 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 374 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 375 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 376 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 377 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 378 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 379 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 380 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 381 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 382 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 383 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 384 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 385 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 386 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 387 | golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= 388 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 389 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q= 390 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 391 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 392 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 393 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 394 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 395 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 396 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 397 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 398 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 399 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 400 | golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 401 | golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 402 | golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 403 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 404 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 405 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 406 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 407 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 408 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 409 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 410 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 411 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 412 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 413 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 414 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 415 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 416 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 417 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 418 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 419 | golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 420 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 421 | golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 422 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 423 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 424 | golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 425 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 426 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 427 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 428 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 429 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 430 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 431 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 432 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 433 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 434 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 435 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 436 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 437 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 438 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 439 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 440 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 441 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 442 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 443 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 444 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 445 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 446 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 447 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 448 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 449 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 450 | golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 451 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 452 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 453 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 454 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 455 | golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 456 | golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 457 | golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 458 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 459 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 460 | golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 461 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 462 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 463 | golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 464 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 465 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= 466 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 467 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 468 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 469 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 470 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 471 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 472 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 473 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 474 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 475 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 476 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 477 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 478 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 479 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 480 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 481 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 482 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 483 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 484 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 485 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 486 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 487 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 488 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 489 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 490 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 491 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 492 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 493 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 494 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 495 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 496 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 497 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 498 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 499 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 500 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 501 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 502 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 503 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 504 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 505 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 506 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 507 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 508 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 509 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 510 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 511 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 512 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 513 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 514 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 515 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 516 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 517 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 518 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 519 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 520 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 521 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 522 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 523 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 524 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 525 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 526 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 527 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 528 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 529 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 530 | golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 531 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 532 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 533 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 534 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 535 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 536 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 537 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 538 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 539 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 540 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 541 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 542 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 543 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 544 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 545 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 546 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 547 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 548 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 549 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 550 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 551 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 552 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 553 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 554 | google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= 555 | google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= 556 | google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= 557 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 558 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 559 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 560 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 561 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 562 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 563 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 564 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 565 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 566 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 567 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 568 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 569 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 570 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 571 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 572 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 573 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 574 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 575 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 576 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 577 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 578 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 579 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 580 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 581 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 582 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 583 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 584 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 585 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 586 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 587 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 588 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 589 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 590 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 591 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 592 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 593 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 594 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 595 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 596 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 597 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 598 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 599 | google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 600 | google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 601 | google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 602 | google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 603 | google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= 604 | google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= 605 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 606 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 607 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 608 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 609 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 610 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 611 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 612 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 613 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 614 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 615 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 616 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 617 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 618 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 619 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 620 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 621 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 622 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 623 | google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 624 | google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= 625 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 626 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 627 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 628 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 629 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 630 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 631 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 632 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 633 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 634 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 635 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 636 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 637 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 638 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 639 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 640 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 641 | gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= 642 | gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 643 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 644 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 645 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 646 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 647 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 648 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 649 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 650 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 651 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 652 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 653 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 654 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 655 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 656 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 657 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 658 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 659 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 660 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 661 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 662 | -------------------------------------------------------------------------------- /httpd/handler.go: -------------------------------------------------------------------------------- 1 | package httpd 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "io" 7 | "net" 8 | "net/http" 9 | "net/http/httputil" 10 | "net/url" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "text/template" 15 | "time" 16 | 17 | mfest "github.com/DSpeichert/netbootd/manifest" 18 | "github.com/DSpeichert/netbootd/static" 19 | "github.com/Masterminds/sprig" 20 | ) 21 | 22 | type Handler struct { 23 | server *Server 24 | } 25 | 26 | func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 27 | ip, _, err := net.SplitHostPort(r.RemoteAddr) 28 | raddr := net.ParseIP(ip) 29 | 30 | h.server.logger.Info(). 31 | Str("path", r.RequestURI). 32 | Str("client", raddr.String()). 33 | Msg("incoming HTTP request") 34 | 35 | manifestRaddr := raddr 36 | spoofIPs, ok := r.URL.Query()["spoof"] 37 | if ok && len(spoofIPs[0]) > 0 { 38 | manifestRaddr = net.ParseIP(spoofIPs[0]) 39 | } 40 | 41 | manifest := h.server.store.FindByIP(manifestRaddr) 42 | if manifest == nil { 43 | h.server.logger.Info(). 44 | Str("path", r.RequestURI). 45 | Str("client", raddr.String()). 46 | Str("manifest_for", manifestRaddr.String()). 47 | Msg("no manifest for client") 48 | http.Error(w, "no manifest for client: "+raddr.String(), http.StatusNotFound) 49 | return 50 | } 51 | 52 | if manifest.Ipxe { 53 | f, err := static.Files.Open(strings.TrimLeft(r.URL.Path, "/")) 54 | if err == nil { 55 | fstat, _ := f.Stat() 56 | h.server.logger.Info(). 57 | Err(err). 58 | Str("path", r.RequestURI). 59 | Str("client", raddr.String()). 60 | Str("manifest_for", manifestRaddr.String()). 61 | Msg("static download") 62 | 63 | http.ServeContent(w, r, fstat.Name(), fstat.ModTime(), f.(io.ReadSeeker)) 64 | return 65 | } 66 | } 67 | 68 | mount, err := manifest.GetMount(r.URL.Path) 69 | if err != nil { 70 | h.server.logger.Error(). 71 | Err(err). 72 | Str("path", r.URL.Path). 73 | Str("client", raddr.String()). 74 | Str("manifest_for", manifestRaddr.String()). 75 | Msg("cannot find mount") 76 | 77 | http.NotFound(w, r) 78 | return 79 | } 80 | 81 | h.server.logger.Trace(). 82 | Interface("mount", mount). 83 | Msg("found mount") 84 | 85 | if mount.Content != "" { 86 | tmpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(mount.Content) 87 | if err != nil { 88 | h.server.logger.Error(). 89 | Err(err). 90 | Msg("failed to parse content template for mount") 91 | http.Error(w, err.Error(), http.StatusInternalServerError) 92 | return 93 | } 94 | 95 | buf := new(bytes.Buffer) 96 | 97 | err = tmpl.Execute(buf, mfest.ContentContext{ 98 | RemoteIP: raddr, 99 | HttpBaseUrl: &url.URL{ 100 | Scheme: "http", 101 | Host: r.Host, 102 | }, 103 | Manifest: manifest, 104 | }) 105 | if err != nil { 106 | h.server.logger.Error(). 107 | Err(err). 108 | Msg("failed to execute content template for mount") 109 | http.Error(w, err.Error(), http.StatusInternalServerError) 110 | return 111 | } 112 | 113 | http.ServeContent(w, r, mount.Path, time.Time{}, bytes.NewReader(buf.Bytes())) 114 | 115 | h.server.logger.Info(). 116 | Err(err). 117 | Str("path", r.RequestURI). 118 | Str("client", raddr.String()). 119 | Str("manifest_for", manifestRaddr.String()). 120 | Msg("transfer finished") 121 | } else if mount.Proxy != "" { 122 | d, err := mount.ProxyDirector() 123 | if err != nil { 124 | h.server.logger.Error(). 125 | Err(err). 126 | Msg("failed to parse proxy URL") 127 | http.Error(w, err.Error(), http.StatusInternalServerError) 128 | return 129 | } 130 | rp := httputil.ReverseProxy{ 131 | Director: d, 132 | } 133 | rp.ServeHTTP(w, r) 134 | return 135 | } else if mount.LocalDir != "" { 136 | path := filepath.Join(mount.LocalDir, mount.Path) 137 | 138 | if mount.AppendSuffix { 139 | path = filepath.Join(mount.LocalDir, strings.TrimPrefix(r.URL.Path, mount.Path)) 140 | } 141 | 142 | if !strings.HasPrefix(path, mount.LocalDir) { 143 | h.server.logger.Error(). 144 | Err(err). 145 | Msgf("Requested path is invalid: %q", path) 146 | http.Error(w, err.Error(), http.StatusBadRequest) 147 | return 148 | } 149 | 150 | f, err := os.Open(path) 151 | if err != nil { 152 | h.server.logger.Error(). 153 | Err(err). 154 | Msgf("Could not get file from local dir: %q", path) 155 | http.Error(w, err.Error(), http.StatusInternalServerError) 156 | return 157 | } 158 | stat, err := f.Stat() 159 | if err != nil { 160 | h.server.logger.Error(). 161 | Err(err). 162 | Msgf("could not stat file: %q", path) 163 | http.Error(w, err.Error(), http.StatusInternalServerError) 164 | return 165 | } 166 | http.ServeContent(w, r, r.URL.Path, stat.ModTime(), f) 167 | return 168 | } else { 169 | // mount has neither .Path, .Proxy nor .LocalDir defined 170 | h.server.logger.Error(). 171 | Str("path", r.RequestURI). 172 | Str("client", raddr.String()). 173 | Str("manifest_for", manifestRaddr.String()). 174 | Str("mount", mount.Path). 175 | Msg("mount is empty") 176 | 177 | http.Error(w, "empty mount", http.StatusInternalServerError) 178 | return 179 | 180 | } 181 | 182 | return 183 | } 184 | -------------------------------------------------------------------------------- /httpd/server.go: -------------------------------------------------------------------------------- 1 | package httpd 2 | 3 | import ( 4 | "github.com/DSpeichert/netbootd/store" 5 | "github.com/rs/zerolog" 6 | "github.com/rs/zerolog/log" 7 | "net" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | type Server struct { 13 | httpClient *http.Client 14 | httpServer *http.Server 15 | 16 | logger zerolog.Logger 17 | store *store.Store 18 | } 19 | 20 | func NewServer(store *store.Store) (server *Server, err error) { 21 | 22 | server = &Server{ 23 | httpServer: &http.Server{ 24 | ReadTimeout: 10 * time.Second, 25 | MaxHeaderBytes: 1 << 20, 26 | IdleTimeout: 10 * time.Second, 27 | }, 28 | logger: log.With().Str("service", "http").Logger(), 29 | store: store, 30 | } 31 | 32 | server.httpServer.Handler = Handler{server: server} 33 | 34 | return server, nil 35 | } 36 | 37 | func (server *Server) Serve(l net.Listener) error { 38 | return server.httpServer.Serve(l) 39 | } 40 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/DSpeichert/netbootd/cmd" 5 | ) 6 | 7 | //go:generate protoc proto/*.proto --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative 8 | 9 | func main() { 10 | cmd.Execute() 11 | } 12 | -------------------------------------------------------------------------------- /manifest/io.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "gopkg.in/yaml.v2" 7 | "path/filepath" 8 | ) 9 | 10 | func ManifestFromJson(content []byte) (manifest Manifest, err error) { 11 | err = json.Unmarshal(content, &manifest) 12 | if err != nil { 13 | return manifest, err 14 | } 15 | 16 | return manifest, manifest.Validate() 17 | } 18 | 19 | func (m *Manifest) ToJson() ([]byte, error) { 20 | return json.MarshalIndent(m, "", " ") 21 | } 22 | 23 | func ManifestFromYaml(content []byte) (manifest Manifest, err error) { 24 | err = yaml.Unmarshal(content, &manifest) 25 | if err != nil { 26 | return manifest, err 27 | } 28 | 29 | return manifest, manifest.Validate() 30 | } 31 | 32 | func (m Manifest) Validate() error { 33 | for _, mount := range m.Mounts { 34 | if mount.LocalDir != "" { 35 | if !filepath.IsAbs(mount.LocalDir) { 36 | return fmt.Errorf("localDir needs to be absolute path") 37 | } 38 | } 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func (m *Manifest) ToYaml() ([]byte, error) { 45 | return yaml.Marshal(&m) 46 | } 47 | -------------------------------------------------------------------------------- /manifest/ipnet.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | type IPWithNet struct { 8 | IP net.IP 9 | Net net.IPNet 10 | } 11 | 12 | func (n *IPWithNet) String() string { 13 | return n.IP.String() + "/" + n.Net.Mask.String() 14 | } 15 | 16 | // MarshalText implements encoding.TextMarshaler using the 17 | // standard CIDR representation of a IPNet. 18 | func (n *IPWithNet) MarshalText() ([]byte, error) { 19 | return []byte(n.String()), nil 20 | } 21 | 22 | // UnmarshalText implements encoding.TextUnmarshaler. 23 | func (n *IPWithNet) UnmarshalText(text []byte) error { 24 | if len(text) == 0 { 25 | *n = IPWithNet{} 26 | return nil 27 | } 28 | 29 | ip, ipnet, err := net.ParseCIDR(string(text)) 30 | if err != nil { 31 | return err 32 | } 33 | *n = IPWithNet{ 34 | IP: ip, 35 | Net: *ipnet, 36 | } 37 | return nil 38 | } 39 | 40 | func (n *IPWithNet) Netmask() string { 41 | return net.IP(n.Net.Mask).String() 42 | } 43 | -------------------------------------------------------------------------------- /manifest/mac.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | // 5 | // Standard net.HardwareAddr cannot be easily marshalled. 6 | // 7 | // See: 8 | // * https://github.com/golang/go/issues/29678 9 | // * https://go-review.googlesource.com/c/go/+/196817/ 10 | 11 | package manifest 12 | 13 | import ( 14 | "net" 15 | ) 16 | 17 | // A HardwareAddr represents a physical hardware address. 18 | type HardwareAddr []byte 19 | 20 | func (a HardwareAddr) String() string { 21 | return net.HardwareAddr(a).String() 22 | } 23 | 24 | // ParseMAC parses s as an IEEE 802 MAC-48, EUI-48, EUI-64, or a 20-octet 25 | // IP over InfiniBand link-layer address using one of the following formats: 26 | // 00:00:5e:00:53:01 27 | // 02:00:5e:10:00:00:00:01 28 | // 00:00:00:00:fe:80:00:00:00:00:00:00:02:00:5e:10:00:00:00:01 29 | // 00-00-5e-00-53-01 30 | // 02-00-5e-10-00-00-00-01 31 | // 00-00-00-00-fe-80-00-00-00-00-00-00-02-00-5e-10-00-00-00-01 32 | // 0000.5e00.5301 33 | // 0200.5e10.0000.0001 34 | // 0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001 35 | func ParseMAC(s string) (hw HardwareAddr, err error) { 36 | hwTmp, err := net.ParseMAC(s) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return HardwareAddr(hwTmp), nil 41 | } 42 | 43 | // MarshalText implements encoding.TextMarshaler using the 44 | // standard string representation of a HardwareAddr. 45 | func (a HardwareAddr) MarshalText() ([]byte, error) { 46 | return []byte(a.String()), nil 47 | } 48 | 49 | // UnmarshalText implements encoding.TextUnmarshaler. 50 | func (a *HardwareAddr) UnmarshalText(text []byte) error { 51 | if len(text) == 0 { 52 | *a = nil 53 | return nil 54 | } 55 | 56 | v, err := ParseMAC(string(text)) 57 | if err != nil { 58 | return err 59 | } 60 | *a = v 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /manifest/schema.go: -------------------------------------------------------------------------------- 1 | package manifest 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // Manifest represents user-supplied per-host manifest information. 13 | // go-yaml accepts completely lowercase version of keys but is not case-insensitive 14 | // https://github.com/go-yaml/yaml/issues/123 15 | // some fields are forcefully mapped to camelCase instead of CamelCase and camelcase 16 | type Manifest struct { 17 | ID string `yaml:"id"` 18 | IPv4 IPWithNet `yaml:"ipv4"` 19 | Hostname string `yaml:"hostname"` 20 | Domain string `yaml:"domain"` 21 | LeaseDuration time.Duration `yaml:"leaseDuration"` 22 | MAC []HardwareAddr 23 | DNS []net.IP 24 | Router []net.IP 25 | NTP []net.IP 26 | Ipxe bool 27 | BootFilename string `yaml:"bootFilename"` 28 | Mounts []Mount 29 | Suspended bool 30 | } 31 | 32 | // Mount represents a path exposed via TFTP and HTTP. 33 | type Mount struct { 34 | // Path at which to select this mount. 35 | Path string 36 | 37 | // If Prefix is set to true, the Path is treated as a prefix. 38 | PathIsPrefix bool `yaml:"pathIsPrefix"` 39 | 40 | // The proxy destination used when handling requests. 41 | // Mutually exclusive with Content option. 42 | Proxy string 43 | // If PathIsPrefix is true and AppendSuffix is true, the suffix to Path Prefix will also be appended to Proxy Or LocalDir. 44 | // Otherwise, it will be many to one proxy. 45 | AppendSuffix bool `yaml:"appendSuffix"` 46 | 47 | // Provides content template (passed through template/text) to serve. 48 | // Mutually exclusive with Proxy option. 49 | Content string 50 | 51 | // Provides a path on the host to find the files. 52 | // So that LocalDir: /tftpboot path: /subdir and client requests: /subdir/file.x the path on the host 53 | // becomes /tfptboot/file.x 54 | LocalDir string `yaml:"localDir"` 55 | } 56 | 57 | func (m Mount) ProxyDirector() (func(req *http.Request), error) { 58 | target, err := url.Parse(m.Proxy) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | // we're not removing the possible "spoof" query param 64 | director := func(req *http.Request) { 65 | //requestDump, err := httputil.DumpRequest(req, true) 66 | //if err != nil { 67 | // fmt.Println(err) 68 | //} 69 | //fmt.Println("original request: " + string(requestDump)) 70 | 71 | req.URL.Scheme = target.Scheme 72 | req.URL.Host = target.Host 73 | req.Host = target.Host 74 | 75 | if _, ok := req.Header["User-Agent"]; !ok { 76 | // explicitly disable User-Agent so it's not set to default value 77 | req.Header.Set("User-Agent", "") 78 | } 79 | 80 | if m.AppendSuffix { 81 | req.URL.Path = target.Path + strings.TrimPrefix(req.URL.Path, m.Path) 82 | req.URL.RawPath = target.RawPath + strings.TrimPrefix(req.URL.RawPath, m.Path) 83 | } else { 84 | req.URL.Path = target.Path 85 | req.URL.RawPath = target.RawPath 86 | } 87 | 88 | //requestDump, err = httputil.DumpRequest(req, true) 89 | //if err != nil { 90 | // fmt.Println(err) 91 | //} 92 | //fmt.Println("modified request: " + string(requestDump)) 93 | } 94 | 95 | return director, nil 96 | } 97 | 98 | // ContentContext is the template context available for static Content embedded in Manifests. 99 | type ContentContext struct { 100 | // Address of netbootd server 101 | LocalIP net.IP 102 | // Address of client 103 | RemoteIP net.IP 104 | // Base URL to the HTTP service (IP and port) - not API 105 | HttpBaseUrl *url.URL 106 | // Copy of Manifest 107 | Manifest *Manifest 108 | } 109 | 110 | // GetMount returns best matching Mount, respecting exact and prefix-based mount paths. 111 | // Longest path match is considered "best". 112 | // If the path in the Mount or being matched begins with a slash (/), it is ignored. 113 | func (m *Manifest) GetMount(path string) (Mount, error) { 114 | path = strings.TrimLeft(path, "/") 115 | var bestMount Mount 116 | var found bool 117 | for _, mount := range m.Mounts { 118 | mountPath := strings.TrimLeft(mount.Path, "/") 119 | if !mount.PathIsPrefix && mountPath == path { 120 | return mount, nil 121 | } else if mount.PathIsPrefix && 122 | (mountPath == "" || strings.HasPrefix(path, mountPath)) && 123 | (len(mount.Path) > len(bestMount.Path) || !found) { 124 | bestMount = mount 125 | found = true 126 | } 127 | } 128 | 129 | if found { 130 | return bestMount, nil 131 | } 132 | return bestMount, errors.New("no mount matches path: " + path) 133 | } 134 | -------------------------------------------------------------------------------- /netbootd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=netboot daemon includes DHCP, TFTP and HTTP service 3 | After=network.target 4 | 5 | [Service] 6 | Type=notify 7 | ExecStart=/usr/bin/netbootd server --trace 8 | #AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_RAW 9 | #User=nobody 10 | #Group=nobody 11 | 12 | [Install] 13 | WantedBy=multi-user.target 14 | -------------------------------------------------------------------------------- /netbootd.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # This is a sample config file for netbootd in YAML format. 3 | # You can use any format supported by https://github.com/spf13/viper. 4 | # Place this file in /etc/netbootd/ or current working directory. 5 | 6 | # Set address to listen on (DHCP, TFTP & HTTP) 7 | #address: 0.0.0.0 8 | 9 | # Set interface to listen on 10 | #interface: eth0 11 | 12 | # debug logging 13 | debug: false 14 | 15 | # trace logging (most verbose) 16 | trace: false 17 | 18 | api: 19 | port: 8081 20 | 21 | # API endpoints can require static authentication via the HTTP "Authorization" header. 22 | # Uncomment the following line and the value will be required in all API requests (except self-action). 23 | #authorization: Bearer secretkey 24 | 25 | # In order for API to use TLS, uncomment and set the following to the correct paths. 26 | #TLSPrivateKeyPath: /etc/ssl/private.key 27 | #TLSCertificatePath: /etc/ssl/certificate.pem 28 | 29 | http: 30 | port: 8080 31 | 32 | # Set to directory from which initial manifests will be loaded at startup 33 | #manifestPath: /etc/netbootd/manifests/ 34 | -------------------------------------------------------------------------------- /static/README.md: -------------------------------------------------------------------------------- 1 | This package embeds [iPXE](https://ipxe.org), which is available 2 | under [multiple free-software licenses](https://ipxe.org/licensing). -------------------------------------------------------------------------------- /static/files.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import "embed" 4 | 5 | //go:embed ipxe.efi undionly.kpxe ipxe_arm64.efi 6 | var Files embed.FS 7 | -------------------------------------------------------------------------------- /static/ipxe.efi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DSpeichert/netbootd/b24d40b8efa1e26e0deddd13ff2e9284a94d7ea0/static/ipxe.efi -------------------------------------------------------------------------------- /static/ipxe_arm64.efi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DSpeichert/netbootd/b24d40b8efa1e26e0deddd13ff2e9284a94d7ea0/static/ipxe_arm64.efi -------------------------------------------------------------------------------- /static/undionly.kpxe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DSpeichert/netbootd/b24d40b8efa1e26e0deddd13ff2e9284a94d7ea0/static/undionly.kpxe -------------------------------------------------------------------------------- /store/persistence.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "github.com/DSpeichert/netbootd/manifest" 5 | ) 6 | 7 | func (s *Store) putPersistentManifest(m manifest.Manifest) error { 8 | 9 | return nil 10 | } 11 | 12 | func (s *Store) forgetPersistentManifest(id string) error { 13 | 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "errors" 5 | "github.com/DSpeichert/netbootd/manifest" 6 | "github.com/rs/zerolog" 7 | "github.com/rs/zerolog/log" 8 | "net" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | type Config struct { 16 | PersistenceDirectory string 17 | } 18 | 19 | type Store struct { 20 | config Config 21 | 22 | // mapping Manifest ID to Manifest 23 | manifests map[string]*manifest.Manifest 24 | 25 | // mapping IP Address to Manifest 26 | // IP is normalized string(ip.To16) 27 | ip map[string]*manifest.Manifest 28 | 29 | // mapping Mac Address to Manifest 30 | mac map[string]*manifest.Manifest 31 | 32 | logger zerolog.Logger 33 | 34 | mutex sync.RWMutex 35 | 36 | // sort of global config 37 | GlobalHints struct { 38 | HttpPort int 39 | } 40 | } 41 | 42 | func NewStore(cfg Config) (*Store, error) { 43 | store := Store{ 44 | config: cfg, 45 | manifests: make(map[string]*manifest.Manifest), 46 | ip: make(map[string]*manifest.Manifest), 47 | mac: make(map[string]*manifest.Manifest), 48 | logger: log.With().Str("module", "store").Logger(), 49 | } 50 | 51 | return &store, nil 52 | } 53 | 54 | func (s *Store) LoadFromDirectory(path string) (err error) { 55 | items, err := os.ReadDir(path) 56 | for _, item := range items { 57 | if !item.Type().IsRegular() || 58 | (!strings.HasSuffix(item.Name(), ".yml") && !strings.HasSuffix(item.Name(), ".yaml")) { 59 | continue 60 | } 61 | 62 | b, err := os.ReadFile(filepath.Join(path, item.Name())) 63 | if err != nil { 64 | s.logger.Error(). 65 | Err(err). 66 | Msg("cannot open file") 67 | continue 68 | } 69 | m, err := manifest.ManifestFromYaml(b) 70 | if err != nil { 71 | s.logger.Error(). 72 | Err(err). 73 | Msg("cannot parse YAML manifest") 74 | continue 75 | } 76 | err = s.PutManifest(m) 77 | if err != nil { 78 | s.logger.Error(). 79 | Err(err). 80 | Msg("cannot add manifest to store") 81 | continue 82 | } 83 | 84 | if s.logger.Debug().Enabled() { 85 | s.logger.Debug(). 86 | Interface("manifest", m). 87 | Msg("Loaded manifest from file") 88 | } 89 | 90 | } 91 | return 92 | } 93 | 94 | func (s *Store) PutManifest(m manifest.Manifest) error { 95 | if m.IPv4.IP == nil { 96 | return errors.New("no IPv4 address provided") 97 | } 98 | 99 | if m.ID == "" { 100 | return errors.New("ID cannot be null") 101 | } 102 | 103 | s.mutex.Lock() 104 | defer s.mutex.Unlock() 105 | 106 | s.manifests[m.ID] = &m 107 | s.ip[string(m.IPv4.IP.To16())] = &m 108 | for _, mac := range m.MAC { 109 | s.mac[mac.String()] = &m 110 | } 111 | 112 | if s.config.PersistenceDirectory != "" { 113 | return s.putPersistentManifest(m) 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func (s *Store) ForgetManifest(id string) error { 120 | s.mutex.RLock() 121 | m, ok := s.manifests[id] 122 | s.mutex.RUnlock() 123 | if !ok { 124 | return nil 125 | } 126 | 127 | s.mutex.Lock() 128 | defer s.mutex.Unlock() 129 | 130 | delete(s.manifests, m.ID) 131 | delete(s.ip, string(m.IPv4.IP.To16())) 132 | for _, mac := range m.MAC { 133 | delete(s.mac, mac.String()) 134 | } 135 | 136 | if s.config.PersistenceDirectory != "" { 137 | return s.forgetPersistentManifest(id) 138 | } 139 | 140 | return nil 141 | } 142 | 143 | func (s *Store) Find(id string) *manifest.Manifest { 144 | s.mutex.RLock() 145 | defer s.mutex.RUnlock() 146 | 147 | return s.manifests[id] 148 | } 149 | 150 | func (s *Store) FindByIP(ip net.IP) *manifest.Manifest { 151 | s.mutex.RLock() 152 | defer s.mutex.RUnlock() 153 | 154 | return s.ip[string(ip.To16())] 155 | } 156 | 157 | func (s *Store) FindByMAC(mac net.HardwareAddr) *manifest.Manifest { 158 | s.mutex.RLock() 159 | defer s.mutex.RUnlock() 160 | 161 | return s.mac[mac.String()] 162 | } 163 | 164 | func (s *Store) GetAll() map[string]*manifest.Manifest { 165 | s.mutex.RLock() 166 | defer s.mutex.RUnlock() 167 | 168 | return s.manifests 169 | } 170 | -------------------------------------------------------------------------------- /tftpd/handler.go: -------------------------------------------------------------------------------- 1 | package tftpd 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | "text/template" 15 | 16 | mfest "github.com/DSpeichert/netbootd/manifest" 17 | "github.com/DSpeichert/netbootd/static" 18 | "github.com/Masterminds/sprig" 19 | "github.com/pin/tftp" 20 | ) 21 | 22 | func (server *Server) tftpReadHandler(filename string, rf io.ReaderFrom) error { 23 | raddr := rf.(tftp.OutgoingTransfer).RemoteAddr() // net.UDPAddr 24 | laddr := rf.(tftp.RequestPacketInfo).LocalIP() 25 | 26 | server.logger.Info(). 27 | Str("path", filename). 28 | Str("client", raddr.IP.String()). 29 | Msg("new TFTP request") 30 | 31 | manifest := server.store.FindByIP(raddr.IP) 32 | if manifest == nil { 33 | server.logger.Info(). 34 | Str("path", filename). 35 | Str("client", raddr.IP.String()). 36 | Msg("no manifest for client") 37 | return errors.New("no manifest for client: " + raddr.IP.String()) 38 | } 39 | 40 | if manifest.Ipxe { 41 | f, err := static.Files.Open(filename) 42 | if err == nil { 43 | n, err := rf.ReadFrom(f.(io.ReadSeeker)) 44 | server.logger.Info(). 45 | Err(err). 46 | Str("path", filename). 47 | Str("client", raddr.IP.String()). 48 | Int64("sent", n). 49 | Msg("transfer finished") 50 | return nil 51 | } 52 | } 53 | 54 | mount, err := manifest.GetMount(filename) 55 | if err != nil { 56 | server.logger.Error(). 57 | Err(err). 58 | Str("path", filename). 59 | Str("client", raddr.IP.String()). 60 | Msg("cannot find mount") 61 | return err 62 | } 63 | 64 | server.logger.Trace(). 65 | Interface("mount", mount). 66 | Msg("found mount") 67 | 68 | if mount.Proxy != "" { 69 | url := mount.Proxy 70 | if mount.AppendSuffix { 71 | url = url + strings.TrimPrefix(filename, mount.Path) 72 | } 73 | 74 | req, err := http.NewRequest("GET", url, nil) 75 | if err != nil { 76 | server.logger.Error(). 77 | Err(err). 78 | Msg("http request setup failed") 79 | return err 80 | } 81 | req.Header.Add("X-Forwarded-For", raddr.IP.String()) 82 | req.Header.Add("X-TFTP-Port", fmt.Sprintf("%d", raddr.Port)) 83 | req.Header.Add("X-TFTP-File", filename) 84 | resp, err := server.httpClient.Do(req) 85 | if err != nil { 86 | server.logger.Error(). 87 | Err(err). 88 | Msg("http request setup failed") 89 | return err 90 | } 91 | defer resp.Body.Close() 92 | 93 | if resp.StatusCode == http.StatusNotFound { 94 | server.logger.Error(). 95 | Str("url", url). 96 | Str("status", resp.Status). 97 | Str("path", filename). 98 | Str("client", raddr.IP.String()). 99 | Msg("upstream: not found") 100 | return errors.New("file not found") 101 | } else if resp.StatusCode != http.StatusOK { 102 | server.logger.Error(). 103 | Msgf("http request returned status %s", resp.Status) 104 | return fmt.Errorf("HTTP request error: %s", resp.Status) 105 | } 106 | 107 | // Use ContentLength, if provided, to set TSize option 108 | if resp.ContentLength >= 0 { 109 | rf.(tftp.OutgoingTransfer).SetSize(resp.ContentLength) 110 | } 111 | 112 | n, err := rf.ReadFrom(resp.Body) 113 | if err != nil { 114 | server.logger.Error(). 115 | Msgf("ReadFrom failed: %v", err) 116 | return err 117 | } 118 | 119 | server.logger.Info(). 120 | Err(err). 121 | Str("path", filename). 122 | Str("url", url). 123 | Str("client", raddr.IP.String()). 124 | Int64("sent", n). 125 | Msg("transfer finished") 126 | } else if mount.Content != "" { 127 | tmpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(mount.Content) 128 | if err != nil { 129 | server.logger.Error(). 130 | Err(err). 131 | Msg("failed to parse content template for mount") 132 | return err 133 | } 134 | 135 | buf := new(bytes.Buffer) 136 | 137 | err = tmpl.Execute(buf, mfest.ContentContext{ 138 | LocalIP: laddr, 139 | RemoteIP: raddr.IP, 140 | HttpBaseUrl: &url.URL{ 141 | Scheme: "http", 142 | Host: fmt.Sprintf("%s:%d", laddr.String(), server.store.GlobalHints.HttpPort), 143 | }, 144 | Manifest: manifest, 145 | }) 146 | if err != nil { 147 | server.logger.Error(). 148 | Err(err). 149 | Msg("failed to parse content template for mount") 150 | return err 151 | } 152 | 153 | rf.(tftp.OutgoingTransfer).SetSize(int64(buf.Len())) 154 | 155 | n, err := rf.ReadFrom(buf) 156 | if err != nil { 157 | server.logger.Error(). 158 | Msgf("ReadFrom failed: %v", err) 159 | return err 160 | } 161 | 162 | server.logger.Info(). 163 | Err(err). 164 | Str("path", filename). 165 | Str("client", raddr.IP.String()). 166 | Int64("sent", n). 167 | Msg("transfer finished") 168 | } else if mount.LocalDir != "" { 169 | path := filepath.Join(mount.LocalDir, mount.Path) 170 | 171 | if mount.AppendSuffix { 172 | path = filepath.Join(mount.LocalDir, strings.TrimPrefix(filename, mount.Path)) 173 | } 174 | 175 | if !strings.HasPrefix(path, mount.LocalDir) { 176 | err := fmt.Errorf("requested path is invalid") 177 | server.logger.Error(). 178 | Err(err). 179 | Msgf("Requested path is invalid: %q", path) 180 | return err 181 | } 182 | 183 | f, err := os.Open(path) 184 | if err != nil { 185 | server.logger.Error(). 186 | Err(err). 187 | Msgf("Could not get file from local dir: %q", filename) 188 | 189 | return err 190 | } 191 | 192 | stat, err := f.Stat() 193 | if err != nil { 194 | server.logger.Error(). 195 | Err(err). 196 | Msgf("Could not stat file: %q", path) 197 | return err 198 | } 199 | 200 | rf.(tftp.OutgoingTransfer).SetSize(int64(stat.Size())) 201 | 202 | n, err := rf.ReadFrom(f) 203 | if err != nil { 204 | server.logger.Error(). 205 | Msgf("ReadFrom failed: %v", err) 206 | return err 207 | } 208 | 209 | server.logger.Info(). 210 | Err(err). 211 | Str("path", filename). 212 | Str("client", raddr.IP.String()). 213 | Int64("sent", n). 214 | Msg("transfer finished") 215 | } else { 216 | // mount has neither .Path nor .Proxy defined 217 | server.logger.Error(). 218 | Str("path", filename). 219 | Str("client", raddr.IP.String()). 220 | Str("mount", mount.Path). 221 | Msg("mount is empty") 222 | return errors.New("empty mount") 223 | } 224 | 225 | return nil 226 | } 227 | -------------------------------------------------------------------------------- /tftpd/server.go: -------------------------------------------------------------------------------- 1 | package tftpd 2 | 3 | import ( 4 | "github.com/DSpeichert/netbootd/store" 5 | "github.com/pin/tftp" 6 | "github.com/rs/zerolog" 7 | "github.com/rs/zerolog/log" 8 | "net" 9 | "net/http" 10 | ) 11 | 12 | type Server struct { 13 | httpClient *http.Client 14 | tftpServer *tftp.Server 15 | 16 | logger zerolog.Logger 17 | store *store.Store 18 | } 19 | 20 | func NewServer(store *store.Store) (server *Server, err error) { 21 | 22 | server = &Server{ 23 | httpClient: &http.Client{}, 24 | logger: log.With().Str("service", "tftp").Logger(), 25 | store: store, 26 | } 27 | 28 | return server, nil 29 | } 30 | 31 | func (server *Server) Serve(conn *net.UDPConn) { 32 | server.tftpServer = tftp.NewServer(server.tftpReadHandler, nil) 33 | _ = server.tftpServer.Serve(conn) 34 | } 35 | --------------------------------------------------------------------------------