├── .builds └── go-stable.yml ├── LICENSE.md ├── README.md ├── client.go ├── client_internal_test.go ├── client_test.go ├── command.go ├── doc.go ├── error.go ├── go.mod ├── go.sum ├── kvparser.go ├── kvparser_test.go ├── reuseport_linux.go ├── reuseport_others.go ├── server.go └── server_test.go /.builds/go-stable.yml: -------------------------------------------------------------------------------- 1 | image: archlinux 2 | packages: 3 | - go 4 | sources: 5 | - https://github.com/mdlayher/wgdynamic-go 6 | environment: 7 | GO111MODULE: "on" 8 | tasks: 9 | - build: | 10 | go version 11 | go get golang.org/x/lint/golint 12 | go get honnef.co/go/tools/cmd/staticcheck 13 | cd wgdynamic-go/ 14 | /home/build/go/bin/staticcheck ./... 15 | /home/build/go/bin/golint -set_exit_status ./... 16 | go test -v -race ./... 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (C) 2019 Matt Layher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wgdynamic-go [![builds.sr.ht status](https://builds.sr.ht/~mdlayher/wgdynamic-go.svg)](https://builds.sr.ht/~mdlayher/wgdynamic-go?) [![GoDoc](https://godoc.org/github.com/mdlayher/wgdynamic-go?status.svg)](https://godoc.org/github.com/mdlayher/wgdynamic-go) [![Go Report Card](https://goreportcard.com/badge/github.com/mdlayher/wgdynamic-go)](https://goreportcard.com/report/github.com/mdlayher/wgdynamic-go) 2 | 3 | Package `wgdynamic` implements a client and server for the the wg-dynamic 4 | protocol. 5 | 6 | For more information about wg-dynamic, please see: 7 | . 8 | 9 | This project is not affiliated with the WireGuard or wg-dynamic projects. 10 | 11 | MIT Licensed. 12 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package wgdynamic 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "time" 10 | ) 11 | 12 | // port is the well-known port for wg-dynamic. 13 | const port = 970 14 | 15 | // serverIP is the well-known server IPv6 address for wg-dynamic. 16 | var serverIP = &net.IPNet{ 17 | IP: net.ParseIP("fe80::"), 18 | Mask: net.CIDRMask(64, 128), 19 | } 20 | 21 | // A Client can request IP address assignment using the wg-dynamic protocol. 22 | // Most callers should construct a client using NewClient, which will bind to 23 | // well-known addresses for wg-dynamic communications. 24 | type Client struct { 25 | // Dial specifies an optional function used to dial an arbitrary net.Conn 26 | // connection to a predetermined wg-dynamic server. This is only necessary 27 | // when using a net.Conn transport other than *net.TCPConn, and most callers 28 | // should use NewClient to construct a Client instead. 29 | Dial func(ctx context.Context) (net.Conn, error) 30 | } 31 | 32 | // NewClient creates a new Client bound to the specified WireGuard interface. 33 | // NewClient will return an error if the interface does not have an IPv6 34 | // link-local address configured. 35 | func NewClient(iface string) (*Client, error) { 36 | // TODO(mdlayher): verify this is actually a WireGuard device. 37 | ifi, err := net.InterfaceByName(iface) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | addrs, err := ifi.Addrs() 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return newClient(ifi.Name, addrs) 48 | } 49 | 50 | // newClient constructs a Client which communicates using well-known wg-dynamic 51 | // addresses. It is used as an entry point in tests. 52 | func newClient(iface string, addrs []net.Addr) (*Client, error) { 53 | // Find a suitable link-local IPv6 address for wg-dynamic communication. 54 | llip, ok := linkLocalIPv6(addrs) 55 | if !ok { 56 | return nil, fmt.Errorf("wgdynamic: no link-local IPv6 address for interface %q", iface) 57 | } 58 | 59 | // Client will listen on a well-known port and send requests to the 60 | // well-known server address. 61 | return &Client{ 62 | // By default, use the stdlib net.Dialer type. 63 | Dial: func(ctx context.Context) (net.Conn, error) { 64 | d := &net.Dialer{ 65 | // The server expects the client to be bound to a specific 66 | // local address. 67 | LocalAddr: &net.TCPAddr{ 68 | IP: llip.IP, 69 | Port: port, 70 | Zone: iface, 71 | }, 72 | // On Linux, pass SO_REUSEPORT to prevent a nuisance error about 73 | // the port being in use when the client makes a few calls 74 | // in succession. 75 | Control: reusePort, 76 | } 77 | 78 | // wg-dynamic TCP connections always use IPv6. 79 | return d.DialContext(ctx, "tcp6", (&net.TCPAddr{ 80 | IP: serverIP.IP, 81 | Port: port, 82 | Zone: iface, 83 | }).String()) 84 | }, 85 | }, nil 86 | } 87 | 88 | // RequestIP requests IP address assignment from a server. Fields within req 89 | // can be specified to request specific IP address assignment parameters. If req 90 | // is nil, the server will automatically perform IP address assignment. 91 | // 92 | // The provided Context must be non-nil. If the context expires before the 93 | // request is complete, an error is returned. 94 | func (c *Client) RequestIP(ctx context.Context, req *RequestIP) (*RequestIP, error) { 95 | // Don't allow the client to set lease start. 96 | if req != nil && !req.LeaseStart.IsZero() { 97 | return nil, errors.New("wgdynamic: clients cannot specify a lease start time") 98 | } 99 | 100 | // Use a separate variable for the output so we don't overwrite the 101 | // caller's request. 102 | var rip *RequestIP 103 | err := c.execute(ctx, func(rw io.ReadWriter) error { 104 | if err := sendRequestIP(rw, fromClient, req); err != nil { 105 | return err 106 | } 107 | 108 | rrip, err := parseRequestIP(newKVParser(rw)) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | rip = rrip 114 | return nil 115 | }) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | return rip, nil 121 | } 122 | 123 | // deadlineNow is a time in the past that indicates a connection should 124 | // immediately time out. 125 | var deadlineNow = time.Unix(1, 0) 126 | 127 | // execute executes fn with a network connection backing rw. 128 | func (c *Client) execute(ctx context.Context, fn func(rw io.ReadWriter) error) error { 129 | conn, err := c.Dial(ctx) 130 | if err != nil { 131 | return err 132 | } 133 | defer conn.Close() 134 | 135 | // Enable immediate connection cancelation via context by using the context's 136 | // deadline and also setting a deadline in the past if/when the context is 137 | // canceled. This pattern courtesy of @acln from #networking on Gophers Slack. 138 | dl, _ := ctx.Deadline() 139 | if err := conn.SetDeadline(dl); err != nil { 140 | return err 141 | } 142 | 143 | errC := make(chan error) 144 | go func() { errC <- fn(conn) }() 145 | 146 | select { 147 | case <-ctx.Done(): 148 | if ctx.Err() == context.Canceled { 149 | if err := conn.SetDeadline(deadlineNow); err != nil { 150 | return err 151 | } 152 | } 153 | 154 | <-errC 155 | return ctx.Err() 156 | case err := <-errC: 157 | return err 158 | } 159 | } 160 | 161 | // linkLocalIPv6 finds a link-local IPv6 address in addrs. It returns true when 162 | // one is found. 163 | func linkLocalIPv6(addrs []net.Addr) (*net.IPNet, bool) { 164 | var llip *net.IPNet 165 | for _, a := range addrs { 166 | ipn, ok := a.(*net.IPNet) 167 | if !ok { 168 | continue 169 | } 170 | 171 | // Only look for link-local IPv6 addresses. 172 | if ipn.IP.To4() == nil && ipn.IP.IsLinkLocalUnicast() { 173 | llip = ipn 174 | break 175 | } 176 | } 177 | 178 | return llip, llip != nil 179 | } 180 | -------------------------------------------------------------------------------- /client_internal_test.go: -------------------------------------------------------------------------------- 1 | package wgdynamic 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | ) 7 | 8 | func Test_newClient(t *testing.T) { 9 | const iface = "eth0" 10 | 11 | tests := []struct { 12 | name string 13 | addrs []net.Addr 14 | ok bool 15 | }{ 16 | { 17 | name: "no addresses", 18 | }, 19 | { 20 | name: "no suitable addresses", 21 | addrs: []net.Addr{ 22 | // This is nonsensical, but it verifies that a failed type 23 | // assertion won't crash the program. 24 | &net.TCPAddr{}, 25 | // Link-local IPv4 address. 26 | mustIPNet("169.254.0.1/32"), 27 | // Globally routable IPv6 address. 28 | mustIPNet("2001:db8::1/128"), 29 | }, 30 | }, 31 | { 32 | name: "OK", 33 | addrs: []net.Addr{ 34 | // Link-local IPv4 address. 35 | mustIPNet("169.254.0.1/32"), 36 | // Link-local IPv6 address. 37 | mustIPNet("fe80::1/128"), 38 | }, 39 | ok: true, 40 | }, 41 | } 42 | 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | _, err := newClient(iface, tt.addrs) 46 | if err != nil { 47 | if tt.ok { 48 | t.Fatalf("failed to create client: %v", err) 49 | } 50 | 51 | t.Logf("OK error: %v", err) 52 | return 53 | } 54 | if !tt.ok { 55 | t.Fatal("expected an error, but none occurred") 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func mustIPNet(s string) *net.IPNet { 62 | _, ipn, err := net.ParseCIDR(s) 63 | if err != nil { 64 | panicf("failed to parse CIDR: %v", err) 65 | } 66 | 67 | return ipn 68 | } 69 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package wgdynamic_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/google/go-cmp/cmp" 13 | "github.com/mdlayher/wgdynamic-go" 14 | ) 15 | 16 | func TestClientRequestIP(t *testing.T) { 17 | var ( 18 | ipv4 = mustIPNet("192.0.2.1/32") 19 | ipv6 = mustIPNet("2001:db8::1/128") 20 | 21 | ips = []*net.IPNet{ipv4, ipv6} 22 | ) 23 | 24 | tests := []struct { 25 | name, res, req string 26 | in, out *wgdynamic.RequestIP 27 | ok bool 28 | err *wgdynamic.Error 29 | }{ 30 | { 31 | name: "protocol error", 32 | req: "request_ip=1\n\n", 33 | res: `request_ip=1 34 | errno=1 35 | errmsg=Out of IPs 36 | 37 | `, 38 | err: &wgdynamic.Error{ 39 | Number: 1, 40 | Message: "Out of IPs", 41 | }, 42 | }, 43 | { 44 | name: "OK nil ipv4/6", 45 | req: "request_ip=1\n\n", 46 | res: `request_ip=1 47 | ip=192.0.2.1/32 48 | ip=2001:db8::1/128 49 | leasestart=1 50 | leasetime=10 51 | errno=0 52 | 53 | `, 54 | out: &wgdynamic.RequestIP{ 55 | IPs: ips, 56 | }, 57 | ok: true, 58 | }, 59 | { 60 | name: "OK ipv4", 61 | req: "request_ip=1\nip=192.0.2.1/32\n\n", 62 | res: `request_ip=1 63 | ip=192.0.2.1/32 64 | leasestart=1 65 | leasetime=10 66 | errno=0 67 | 68 | `, 69 | in: &wgdynamic.RequestIP{ 70 | IPs: []*net.IPNet{ipv4}, 71 | }, 72 | out: &wgdynamic.RequestIP{ 73 | IPs: []*net.IPNet{ipv4}, 74 | }, 75 | ok: true, 76 | }, 77 | { 78 | name: "OK ipv6", 79 | req: "request_ip=1\nip=2001:db8::1/128\n\n", 80 | res: `request_ip=1 81 | ip=2001:db8::1/128 82 | leasestart=1 83 | leasetime=10 84 | errno=0 85 | 86 | `, 87 | in: &wgdynamic.RequestIP{ 88 | IPs: []*net.IPNet{ipv6}, 89 | }, 90 | out: &wgdynamic.RequestIP{ 91 | IPs: []*net.IPNet{ipv6}, 92 | }, 93 | ok: true, 94 | }, 95 | { 96 | name: "OK ipv4/6", 97 | req: `request_ip=1 98 | ip=192.0.2.1/32 99 | ip=2001:db8::1/128 100 | 101 | `, 102 | res: `request_ip=1 103 | ip=192.0.2.1/32 104 | ip=2001:db8::1/128 105 | leasestart=1 106 | leasetime=10 107 | errno=0 108 | 109 | `, 110 | in: &wgdynamic.RequestIP{ 111 | IPs: ips, 112 | }, 113 | out: &wgdynamic.RequestIP{ 114 | IPs: ips, 115 | }, 116 | ok: true, 117 | }, 118 | { 119 | name: "OK address within subnet", 120 | req: "request_ip=1\n\n", 121 | res: `request_ip=1 122 | ip=2001:db8::ffff/64 123 | leasestart=1 124 | leasetime=10 125 | errno=0 126 | 127 | `, 128 | out: &wgdynamic.RequestIP{ 129 | IPs: []*net.IPNet{mustIPNet("2001:db8::ffff/64")}, 130 | }, 131 | ok: true, 132 | }, 133 | } 134 | 135 | for _, tt := range tests { 136 | t.Run(tt.name, func(t *testing.T) { 137 | c, done := testClient(t, tt.res) 138 | 139 | // Perform request and immediately capture the input sent to the 140 | // server since no more requests will be made. 141 | out, err := c.RequestIP(context.Background(), tt.in) 142 | req := done() 143 | if err != nil { 144 | if tt.ok { 145 | t.Fatalf("failed to request IPs: %v", err) 146 | } 147 | 148 | // Is the error a protocol error? If so, compare it. 149 | if werr, ok := err.(*wgdynamic.Error); ok { 150 | if diff := cmp.Diff(tt.err, werr); diff != "" { 151 | t.Fatalf("unexpected protocol error (-want +got):\n%s", diff) 152 | } 153 | } 154 | 155 | return 156 | } 157 | if !tt.ok { 158 | t.Fatal("expected an error, but none occurred") 159 | } 160 | 161 | if diff := cmp.Diff(tt.req, req); diff != "" { 162 | t.Fatalf("unexpected request (-want +got):\n%s", diff) 163 | } 164 | 165 | // Save some test table duplication. 166 | tt.out.LeaseStart = time.Unix(1, 0) 167 | tt.out.LeaseTime = 10 * time.Second 168 | 169 | if diff := cmp.Diff(tt.out, out); diff != "" { 170 | t.Fatalf("unexpected RequestIP (-want +got):\n%s", diff) 171 | } 172 | }) 173 | } 174 | } 175 | 176 | func TestClientRequestIPBadRequest(t *testing.T) { 177 | // A zero-value Client is sufficient for this test, and will also panic 178 | // if the network is accessed (meaning that the code is broken). 179 | var c wgdynamic.Client 180 | _, err := c.RequestIP(context.Background(), &wgdynamic.RequestIP{ 181 | LeaseStart: time.Unix(1, 0), 182 | }) 183 | if err == nil { 184 | t.Fatal("expected an error but none occurred") 185 | } 186 | } 187 | 188 | func TestClientContextDeadlineExceeded(t *testing.T) { 189 | const dur = 100 * time.Millisecond 190 | 191 | c, done := testServer(t, &wgdynamic.Server{ 192 | RequestIP: func(_ net.Addr, _ *wgdynamic.RequestIP) (*wgdynamic.RequestIP, error) { 193 | // Sleep longer than the client should wait. 194 | time.Sleep(dur * 2) 195 | return nil, nil 196 | }, 197 | }) 198 | defer done() 199 | 200 | ctx, cancel := context.WithTimeout(context.Background(), dur) 201 | defer cancel() 202 | 203 | _, err := c.RequestIP(ctx, nil) 204 | if nerr, ok := err.(net.Error); !ok || !nerr.Timeout() { 205 | t.Fatalf("expected timeout error, but got: %v", err) 206 | } 207 | } 208 | 209 | func TestClientContextCanceled(t *testing.T) { 210 | const dur = 100 * time.Millisecond 211 | 212 | c, done := testServer(t, &wgdynamic.Server{ 213 | RequestIP: func(_ net.Addr, _ *wgdynamic.RequestIP) (*wgdynamic.RequestIP, error) { 214 | // Sleep longer than the client should wait. 215 | time.Sleep(dur * 2) 216 | return nil, nil 217 | }, 218 | }) 219 | defer done() 220 | 221 | ctx, cancel := context.WithCancel(context.Background()) 222 | 223 | go func() { 224 | <-time.After(dur) 225 | cancel() 226 | }() 227 | 228 | _, got := c.RequestIP(ctx, nil) 229 | if diff := cmp.Diff(context.Canceled.Error(), got.Error()); diff != "" { 230 | t.Fatalf("unexpected error (-want +got):\n%s", diff) 231 | } 232 | } 233 | 234 | // testClient creates an ephemeral test client and server. The server will 235 | // return res for the first method invoked on Client. 236 | // 237 | // Invoke the cleanup closure to close all connections and return the client's 238 | // raw request. 239 | func testClient(t *testing.T, res string) (*wgdynamic.Client, func() string) { 240 | t.Helper() 241 | 242 | l, err := net.Listen("tcp", ":0") 243 | if err != nil { 244 | t.Fatalf("failed to listen: %v", err) 245 | } 246 | 247 | var wg sync.WaitGroup 248 | wg.Add(1) 249 | 250 | // Used to capture the client's request and return it to the caller. 251 | reqC := make(chan string, 1) 252 | go func() { 253 | defer wg.Done() 254 | 255 | c, err := l.Accept() 256 | if err != nil { 257 | panicf("failed to accept: %v", err) 258 | } 259 | defer c.Close() 260 | 261 | // Capture the request and return a canned response. 262 | b := make([]byte, 128) 263 | n, err := c.Read(b) 264 | if err != nil { 265 | panicf("failed to read request: %v", err) 266 | } 267 | reqC <- string(b[:n]) 268 | 269 | if _, err := io.WriteString(c, res); err != nil { 270 | panicf("failed to write response: %v", err) 271 | } 272 | }() 273 | 274 | // Point the Client at our ephemeral server. 275 | c := &wgdynamic.Client{ 276 | Dial: func(ctx context.Context) (net.Conn, error) { 277 | var d net.Dialer 278 | return d.DialContext(ctx, "tcp", l.Addr().String()) 279 | }, 280 | } 281 | 282 | return c, func() string { 283 | defer close(reqC) 284 | 285 | wg.Wait() 286 | 287 | if err := l.Close(); err != nil { 288 | t.Fatalf("failed to close listener: %v", err) 289 | } 290 | 291 | return <-reqC 292 | } 293 | } 294 | 295 | func mustIPNet(s string) *net.IPNet { 296 | ip, ipn, err := net.ParseCIDR(s) 297 | if err != nil { 298 | panicf("failed to parse CIDR: %v", err) 299 | } 300 | 301 | // See commment in kvParser.IPNet. 302 | ipn.IP = ip 303 | 304 | return ipn 305 | } 306 | 307 | func panicf(format string, a ...interface{}) { 308 | panic(fmt.Sprintf(format, a...)) 309 | } 310 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package wgdynamic 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net" 8 | "time" 9 | ) 10 | 11 | // RequestIP contains IP address requests or assignments, depending on whether 12 | // the structure originated with a client or server. 13 | type RequestIP struct { 14 | // IPs specify IP addresses with subnet masks. 15 | // 16 | // For clients, these request that specific IP addresses are assigned to 17 | // the client. If nil, no specific IP addresses are requested. 18 | // 19 | // For servers, these specify the IP address assignments which are sent 20 | // to a client. If nil, no IP addresses will be specified. 21 | IPs []*net.IPNet 22 | 23 | // LeaseStart specifies the time that an IP address lease begins. 24 | // 25 | // This option only applies to servers and an error will be returned if it 26 | // is used in a client request. 27 | LeaseStart time.Time 28 | 29 | // LeaseTime specifies the duration of an IP address lease. It can be used 30 | // along with LeaseStart to calculate when a lease expires. 31 | // 32 | // For clients, it indicates that the client would prefer a lease for at 33 | // least this duration of time. 34 | // 35 | // For servers, it indicates that the IP address assignment expires after 36 | // this duration of time has elapsed. 37 | LeaseTime time.Duration 38 | } 39 | 40 | // Indicates if a command originates from client or server since the two are 41 | // marshaled into slightly different forms. 42 | const ( 43 | fromServer = false 44 | fromClient = true 45 | ) 46 | 47 | // TODO(mdlayher): request_ip protocol version is hardcoded at 1 and should 48 | // be parameterized in some way. 49 | 50 | // sendRequestIP writes a request_ip command with optional IPv4/6 addresses 51 | // to w. 52 | func sendRequestIP(w io.Writer, isClient bool, rip *RequestIP) error { 53 | if rip == nil { 54 | // No additional parameters to send. 55 | _, err := w.Write([]byte("request_ip=1\n\n")) 56 | return err 57 | } 58 | 59 | // Build the command and attach optional parameters. 60 | var b bytes.Buffer 61 | if isClient { 62 | // Only clients issue the command header. 63 | b.WriteString("request_ip=1\n") 64 | } 65 | 66 | for _, ip := range rip.IPs { 67 | b.WriteString(fmt.Sprintf("ip=%s\n", ip.String())) 68 | } 69 | 70 | if !rip.LeaseStart.IsZero() { 71 | b.WriteString(fmt.Sprintf("leasestart=%d\n", rip.LeaseStart.Unix())) 72 | } 73 | if rip.LeaseTime > 0 { 74 | b.WriteString(fmt.Sprintf("leasetime=%d\n", int(rip.LeaseTime.Seconds()))) 75 | } 76 | 77 | // A final newline completes the request. 78 | b.WriteString("\n") 79 | 80 | _, err := b.WriteTo(w) 81 | return err 82 | } 83 | 84 | // parseRequestIP parses a RequestIP from a request_ip command response stream. 85 | func parseRequestIP(p *kvParser) (*RequestIP, error) { 86 | var rip RequestIP 87 | for p.Next() { 88 | switch p.Key() { 89 | case "ip": 90 | rip.IPs = append(rip.IPs, p.IPNet()) 91 | case "leasestart": 92 | rip.LeaseStart = time.Unix(int64(p.Int()), 0) 93 | case "leasetime": 94 | rip.LeaseTime = time.Duration(p.Int()) * time.Second 95 | } 96 | } 97 | 98 | if err := p.Err(); err != nil { 99 | return nil, err 100 | } 101 | 102 | return &rip, nil 103 | } 104 | 105 | // parseRequest begins the parsing process for reading a client request, returning 106 | // a kvParser and the command being performed. 107 | func parseRequest(r io.Reader) (*kvParser, string, error) { 108 | // Consume the first line to retrieve the command. 109 | p := newKVParser(r) 110 | if !p.Next() { 111 | return nil, "", p.Err() 112 | } 113 | 114 | return p, p.Key(), nil 115 | } 116 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package wgdynamic implements a client and server for the the wg-dynamic 2 | // protocol. 3 | // 4 | // For more information about wg-dynamic, please see: 5 | // https://git.zx2c4.com/wg-dynamic/about/. 6 | // 7 | // This project is not affiliated with the WireGuard or wg-dynamic projects. 8 | package wgdynamic 9 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package wgdynamic 2 | 3 | import "fmt" 4 | 5 | // wg-dynamic defines 0 as "success", but we handle success with nil error 6 | // in Go. 7 | 8 | // Possible Error values. 9 | var ( 10 | ErrInvalidRequest = &Error{ 11 | Number: 1, 12 | Message: "Invalid request", 13 | } 14 | ErrUnsupportedProtocol = &Error{ 15 | Number: 2, 16 | Message: "Unsupported protocol", 17 | } 18 | ErrIPUnavailable = &Error{ 19 | Number: 3, 20 | Message: "Chosen IP(s) unavailable", 21 | } 22 | ) 23 | 24 | var _ error = &Error{} 25 | 26 | // An Error is a wg-dynamic protocol error. 27 | type Error struct { 28 | Number int 29 | Message string 30 | } 31 | 32 | // Error implements error. 33 | func (e *Error) Error() string { 34 | return fmt.Sprintf("wgdynamic: error %d: %s", e.Number, e.Message) 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mdlayher/wgdynamic-go 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/google/go-cmp v0.3.1 7 | golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= 2 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 3 | golang.org/x/sys v0.0.0-20190922100055-0a153f010e69 h1:rOhMmluY6kLMhdnrivzec6lLgaVbMHMn2ISQXJeJ5EM= 4 | golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 5 | -------------------------------------------------------------------------------- /kvparser.go: -------------------------------------------------------------------------------- 1 | package wgdynamic 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "net" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // A kvParser parses streams of key=value pairs. 13 | type kvParser struct { 14 | s *bufio.Scanner 15 | err error 16 | werr Error 17 | k, v string 18 | } 19 | 20 | // newKVParser creates a kvParser that reads from r. 21 | func newKVParser(r io.Reader) *kvParser { 22 | return &kvParser{ 23 | s: bufio.NewScanner(r), 24 | } 25 | } 26 | 27 | // Next advances to the next key=value pair if possible. 28 | func (p *kvParser) Next() bool { 29 | if p.err != nil || !p.s.Scan() || p.s.Text() == "" { 30 | // Hit an error, no more input, or we've reached the end of input. 31 | return false 32 | } 33 | 34 | kvs := strings.Split(p.s.Text(), "=") 35 | if len(kvs) != 2 { 36 | p.err = fmt.Errorf("wgdynamic: malformed key/value pair in response: %q", p.s.Text()) 37 | return false 38 | } 39 | 40 | // Set up internal state for calling other functions. 41 | p.k, p.v = kvs[0], kvs[1] 42 | 43 | // Handle any errors internally and recursively call Next so that the caller 44 | // does not observe any error key/value pairs. 45 | switch p.k { 46 | case "errno": 47 | p.werr.Number = p.Int() 48 | return p.Next() 49 | case "errmsg": 50 | p.werr.Message = p.String() 51 | return p.Next() 52 | } 53 | 54 | return true 55 | } 56 | 57 | // Key returns the current key of a key/value pair. 58 | func (p *kvParser) Key() string { return p.k } 59 | 60 | // Int parses the current value as an integer. 61 | func (p *kvParser) Int() int { 62 | if p.err != nil { 63 | return 0 64 | } 65 | 66 | v, err := strconv.Atoi(p.v) 67 | if err != nil { 68 | p.err = err 69 | return 0 70 | } 71 | 72 | return v 73 | } 74 | 75 | // String returns the current value. 76 | func (p *kvParser) String() string { 77 | if p.err != nil { 78 | return "" 79 | } 80 | 81 | return p.v 82 | } 83 | 84 | // IPNet parses the current value as a *net.IPNet. 85 | func (p *kvParser) IPNet() *net.IPNet { 86 | if p.err != nil { 87 | return nil 88 | } 89 | 90 | ip, ipn, err := net.ParseCIDR(p.v) 91 | if err != nil { 92 | p.err = err 93 | return nil 94 | } 95 | 96 | // We want to return the actual allocated IP address along with its proper 97 | // subnet mask, so replace the first network address with the actual IP 98 | // address. 99 | ipn.IP = ip 100 | 101 | return ipn 102 | } 103 | 104 | // Err returns any errors encountered during parsing. 105 | func (p *kvParser) Err() error { 106 | // First, errors from the underlying scanner. 107 | if err := p.s.Err(); err != nil { 108 | return err 109 | } 110 | 111 | // Next, errors encountered while parsing a value. 112 | if p.err != nil { 113 | return p.err 114 | } 115 | 116 | // Finally, any protocol errors which may have been encountered. 117 | if p.werr.Number != 0 { 118 | return &p.werr 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func panicf(format string, a ...interface{}) { 125 | panic(fmt.Sprintf(format, a...)) 126 | } 127 | -------------------------------------------------------------------------------- /kvparser_test.go: -------------------------------------------------------------------------------- 1 | package wgdynamic 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func Test_kvParserError(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | s string 12 | fn func(p *kvParser) 13 | }{ 14 | { 15 | name: "bad key/value pair", 16 | s: "hello=world\nkey:value\n\n", 17 | fn: func(p *kvParser) { 18 | // Advance to pick up bad key/value pair. 19 | _ = p.Next() 20 | }, 21 | }, 22 | { 23 | name: "bad integer", 24 | s: "hello=string\n\n", 25 | fn: func(p *kvParser) { 26 | _ = p.Int() 27 | }, 28 | }, 29 | { 30 | name: "bad IPNet", 31 | s: "hello=string\n\n", 32 | fn: func(p *kvParser) { 33 | _ = p.IPNet() 34 | }, 35 | }, 36 | } 37 | 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | p := newKVParser(strings.NewReader(tt.s)) 41 | 42 | // Advance to the first line of input and then call into the test 43 | // function to generate errors. 44 | _ = p.Next() 45 | tt.fn(p) 46 | 47 | err := p.Err() 48 | if err == nil { 49 | t.Fatal("expected an error, but none occurred") 50 | } 51 | 52 | t.Logf("OK error: %v", err) 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /reuseport_linux.go: -------------------------------------------------------------------------------- 1 | //+build linux 2 | 3 | package wgdynamic 4 | 5 | import ( 6 | "syscall" 7 | 8 | "golang.org/x/sys/unix" 9 | ) 10 | 11 | func reusePort(_, _ string, c syscall.RawConn) error { 12 | var err error 13 | c.Control(func(fd uintptr) { 14 | err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) 15 | }) 16 | return err 17 | } 18 | -------------------------------------------------------------------------------- /reuseport_others.go: -------------------------------------------------------------------------------- 1 | //+build !linux 2 | 3 | package wgdynamic 4 | 5 | import "syscall" 6 | 7 | func reusePort(_, _ string, _ syscall.RawConn) error { 8 | return nil 9 | } 10 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package wgdynamic 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "net" 9 | "sync" 10 | ) 11 | 12 | // A Server serves wg-dynamic protocol requests. 13 | // 14 | // Each exported function field implements a specific request. If any errors 15 | // are returned, a protocol error is returned to the client. When the error is 16 | // of type *Error, that protocol error is returned to the client. For generic 17 | // errors, a generic protocol error is returned. 18 | type Server struct { 19 | // RequestIP handles requests for IP address assignment. If nil, a generic 20 | // protocol error is returned to the client. 21 | RequestIP func(src net.Addr, r *RequestIP) (*RequestIP, error) 22 | 23 | // Log specifies an error logger for the Server. If nil, all error logs 24 | // are discarded. 25 | Log *log.Logger 26 | 27 | // Guards internal fields set when Serve is first called. 28 | mu sync.Mutex 29 | l net.Listener 30 | wg *sync.WaitGroup 31 | } 32 | 33 | // Listen creates a net.Listener suitable for use with a Server and bound to 34 | // the specified WireGuard interface. Listen will return an error if the 35 | // does not have the well-known IPv6 link-local server address (fe80::/64) 36 | // configured. 37 | func Listen(iface string) (net.Listener, error) { 38 | ifi, err := net.InterfaceByName(iface) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | addrs, err := ifi.Addrs() 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | llip, ok := linkLocalIPv6(addrs) 49 | if !ok || !llip.IP.Equal(serverIP.IP) || !bytes.Equal(llip.Mask, serverIP.Mask) { 50 | return nil, fmt.Errorf("wgdynamic: IPv6 server address %s must be assigned to interface %q", serverIP, iface) 51 | } 52 | 53 | return net.ListenTCP("tcp6", &net.TCPAddr{ 54 | IP: serverIP.IP, 55 | Port: port, 56 | Zone: iface, 57 | }) 58 | } 59 | 60 | // Serve serves incoming requests by accepting connections from l. 61 | func (s *Server) Serve(l net.Listener) error { 62 | // Initialize any necessary fields before starting the listener loop. 63 | s.mu.Lock() 64 | s.l = l 65 | s.wg = &sync.WaitGroup{} 66 | s.mu.Unlock() 67 | 68 | for { 69 | c, err := l.Accept() 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // Guard s.wg to prevent a data race when another goroutine tries to 75 | // wait during a call to Close. 76 | s.mu.Lock() 77 | s.wg.Add(1) 78 | s.mu.Unlock() 79 | 80 | go func() { 81 | defer func() { 82 | // The C implementation immediately closes the connection once 83 | // a request is processed. 84 | _ = c.Close() 85 | s.wg.Done() 86 | }() 87 | 88 | s.handle(c) 89 | }() 90 | } 91 | } 92 | 93 | // Close closes the server listener and waits for all requests to complete. 94 | func (s *Server) Close() error { 95 | s.mu.Lock() 96 | defer s.mu.Unlock() 97 | 98 | defer s.wg.Wait() 99 | return s.l.Close() 100 | } 101 | 102 | // handle handles an individual request. handle should be called in a goroutine. 103 | func (s *Server) handle(c net.Conn) { 104 | p, cmd, err := parseRequest(c) 105 | if err != nil { 106 | s.logf("%s: error parsing request: %v", c.RemoteAddr().String(), err) 107 | return 108 | } 109 | 110 | // Pass the request to the appropriate handler. 111 | switch cmd { 112 | case "request_ip": 113 | err = s.handleRequestIP(c, p) 114 | default: 115 | // No such command. 116 | err = ErrInvalidRequest 117 | } 118 | if err == nil { 119 | // No error handling needed. 120 | return 121 | } 122 | 123 | // If the function returned *Error, use that. Otherwise, log the error and 124 | // specify a generic error. 125 | werr, ok := err.(*Error) 126 | if !ok { 127 | s.logf("%s: %q error: %v", c.RemoteAddr().String(), cmd, err) 128 | werr = ErrInvalidRequest 129 | } 130 | 131 | // TODO(mdlayher): add serialization logic for Error type. 132 | _, _ = io.WriteString(c, fmt.Sprintf("errno=%d\nerrmsg=%s\n\n", 133 | werr.Number, werr.Message)) 134 | } 135 | 136 | // handleRequestIP processes a request_ip command. 137 | func (s *Server) handleRequestIP(c net.Conn, p *kvParser) error { 138 | if s.RequestIP == nil { 139 | // Not implemented by caller. 140 | return ErrInvalidRequest 141 | } 142 | 143 | req, err := parseRequestIP(p) 144 | if err != nil { 145 | return err 146 | } 147 | 148 | res, err := s.RequestIP(c.RemoteAddr(), req) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | return sendRequestIP(c, fromServer, res) 154 | } 155 | 156 | // logf creates a formatted log entry if s.Log is not nil. 157 | func (s *Server) logf(format string, v ...interface{}) { 158 | if s.Log == nil { 159 | return 160 | } 161 | 162 | s.Log.Printf(format, v...) 163 | } 164 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package wgdynamic_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "strings" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/google/go-cmp/cmp" 13 | "github.com/mdlayher/wgdynamic-go" 14 | ) 15 | 16 | type subtest struct { 17 | name string 18 | s *wgdynamic.Server 19 | fn func(t *testing.T, c *wgdynamic.Client) 20 | } 21 | 22 | func TestServer(t *testing.T) { 23 | tests := []struct { 24 | name string 25 | subs []subtest 26 | }{ 27 | { 28 | name: "RequestIP", 29 | subs: requestIPTests(t), 30 | }, 31 | } 32 | 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | for _, st := range tt.subs { 36 | t.Run(st.name, func(t *testing.T) { 37 | c, done := testServer(t, st.s) 38 | defer done() 39 | 40 | st.fn(t, c) 41 | }) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func requestIPTests(t *testing.T) []subtest { 48 | var ( 49 | ipv4 = mustIPNet("192.0.2.1/32") 50 | ipv6 = mustIPNet("2001:db8::1/128") 51 | 52 | ips = []*net.IPNet{ipv4, ipv6} 53 | 54 | want = &wgdynamic.RequestIP{ 55 | IPs: ips, 56 | LeaseStart: time.Unix(1, 0), 57 | LeaseTime: 10 * time.Second, 58 | } 59 | ) 60 | 61 | return []subtest{ 62 | { 63 | name: "not implemented", 64 | s: &wgdynamic.Server{ 65 | RequestIP: nil, 66 | }, 67 | fn: func(t *testing.T, c *wgdynamic.Client) { 68 | _, err := c.RequestIP(context.Background(), nil) 69 | if diff := cmp.Diff(wgdynamic.ErrInvalidRequest, err); diff != "" { 70 | t.Fatalf("unexpected error (-want +got):\n%s", diff) 71 | } 72 | }, 73 | }, 74 | { 75 | name: "generic error", 76 | s: &wgdynamic.Server{ 77 | RequestIP: func(_ net.Addr, _ *wgdynamic.RequestIP) (*wgdynamic.RequestIP, error) { 78 | return nil, errors.New("some error") 79 | }, 80 | }, 81 | fn: func(t *testing.T, c *wgdynamic.Client) { 82 | _, err := c.RequestIP(context.Background(), nil) 83 | if diff := cmp.Diff(wgdynamic.ErrInvalidRequest, err); diff != "" { 84 | t.Fatalf("unexpected error (-want +got):\n%s", diff) 85 | } 86 | }, 87 | }, 88 | { 89 | name: "OK client request", 90 | s: &wgdynamic.Server{ 91 | RequestIP: func(_ net.Addr, r *wgdynamic.RequestIP) (*wgdynamic.RequestIP, error) { 92 | // Return the addresses requested by client, but also 93 | // populate lease time fields. 94 | r.LeaseStart = want.LeaseStart 95 | r.LeaseTime = want.LeaseTime 96 | return r, nil 97 | }, 98 | }, 99 | fn: func(t *testing.T, c *wgdynamic.Client) { 100 | got, err := c.RequestIP(context.Background(), &wgdynamic.RequestIP{ 101 | IPs: ips, 102 | }) 103 | if err != nil { 104 | t.Fatalf("failed to request IP: %v", err) 105 | } 106 | 107 | if diff := cmp.Diff(want, got); diff != "" { 108 | t.Fatalf("unexpected RequestIP (-want +got):\n%s", diff) 109 | } 110 | }, 111 | }, 112 | { 113 | name: "OK auto assign", 114 | s: &wgdynamic.Server{ 115 | RequestIP: func(_ net.Addr, r *wgdynamic.RequestIP) (*wgdynamic.RequestIP, error) { 116 | // Ensure the Client does not request any addresses. 117 | for len(r.IPs) > 0 { 118 | return nil, errors.New("could not assign requested addresses") 119 | } 120 | 121 | return want, nil 122 | }, 123 | }, 124 | fn: func(t *testing.T, c *wgdynamic.Client) { 125 | got, err := c.RequestIP(context.Background(), nil) 126 | if err != nil { 127 | t.Fatalf("failed to request IP: %v", err) 128 | } 129 | 130 | if diff := cmp.Diff(want, got); diff != "" { 131 | t.Fatalf("unexpected RequestIP (-want +got):\n%s", diff) 132 | } 133 | }, 134 | }, 135 | } 136 | } 137 | 138 | func testServer(t *testing.T, s *wgdynamic.Server) (*wgdynamic.Client, func()) { 139 | t.Helper() 140 | 141 | l, err := net.Listen("tcp", ":0") 142 | if err != nil { 143 | t.Fatalf("failed to listen: %v", err) 144 | } 145 | 146 | var wg sync.WaitGroup 147 | wg.Add(1) 148 | 149 | go func() { 150 | defer wg.Done() 151 | 152 | if err := s.Serve(l); err != nil { 153 | if strings.Contains(err.Error(), "use of closed network connection") { 154 | return 155 | } 156 | 157 | panicf("failed to serve: %v", err) 158 | } 159 | }() 160 | 161 | c := &wgdynamic.Client{ 162 | Dial: func(ctx context.Context) (net.Conn, error) { 163 | var d net.Dialer 164 | return d.DialContext(ctx, "tcp", l.Addr().String()) 165 | }, 166 | } 167 | 168 | return c, func() { 169 | defer wg.Wait() 170 | 171 | if err := s.Close(); err != nil { 172 | t.Fatalf("failed to close server listener: %v", err) 173 | } 174 | } 175 | } 176 | --------------------------------------------------------------------------------