|
6 | 6 | "io" |
7 | 7 | "net" |
8 | 8 | "net/http" |
| 9 | + "strconv" |
| 10 | + "strings" |
9 | 11 | "testing" |
10 | 12 | "time" |
11 | 13 |
|
@@ -321,3 +323,138 @@ func TestAuthWildcardChannel(t *testing.T) { |
321 | 323 | } |
322 | 324 | t.Logf("wildcard user correctly allowed") |
323 | 325 | } |
| 326 | + |
| 327 | +// TestAuthSocksChannelDenied verifies that a user who is NOT granted socks |
| 328 | +// cannot open a socks channel, even when the server runs with --socks5. |
| 329 | +// socks channels are authorized against the per-user ACL like any other |
| 330 | +// destination (the channel token is the literal "socks"). |
| 331 | +func TestAuthSocksChannelDenied(t *testing.T) { |
| 332 | + allowedPort := availablePort() |
| 333 | + |
| 334 | + // SOCKS5 is enabled, so any rejection can only come from the ACL, |
| 335 | + // not from the "SOCKS5 is not enabled" guard. |
| 336 | + s, err := chserver.NewServer(&chserver.Config{ |
| 337 | + KeySeed: "acl-socks-denied", |
| 338 | + Socks5: true, |
| 339 | + }) |
| 340 | + if err != nil { |
| 341 | + t.Fatal(err) |
| 342 | + } |
| 343 | + s.Debug = debug |
| 344 | + // user is pinned to a single TCP destination; socks is NOT granted |
| 345 | + if err := s.AddUser("user", "pass", fmt.Sprintf(`^127\.0\.0\.1:%s$`, allowedPort)); err != nil { |
| 346 | + t.Fatal(err) |
| 347 | + } |
| 348 | + serverPort := availablePort() |
| 349 | + if err := s.Start("127.0.0.1", serverPort); err != nil { |
| 350 | + t.Fatal(err) |
| 351 | + } |
| 352 | + defer s.Close() |
| 353 | + |
| 354 | + // declare ONLY the allowed remote so the config handshake passes |
| 355 | + sc, _, _ := dialChiselSSH(t, "127.0.0.1:"+serverPort, "user", "pass") |
| 356 | + defer sc.Close() |
| 357 | + r, err := settings.DecodeRemote(fmt.Sprintf("0.0.0.0:%s:127.0.0.1:%s", allowedPort, allowedPort)) |
| 358 | + if err != nil { |
| 359 | + t.Fatal(err) |
| 360 | + } |
| 361 | + sendConfig(t, sc, []*settings.Remote{r}) |
| 362 | + |
| 363 | + // open a raw channel whose ExtraData is "socks" — must be denied by the ACL |
| 364 | + ch, _, err := sc.OpenChannel("chisel", []byte("socks")) |
| 365 | + if err == nil { |
| 366 | + ch.Close() |
| 367 | + t.Fatal("socks channel was accepted for a user without socks access") |
| 368 | + } |
| 369 | + // it must be the ACL talking, not "SOCKS5 is not enabled" |
| 370 | + if !strings.Contains(err.Error(), "access denied") { |
| 371 | + t.Fatalf("socks channel rejected for the wrong reason: %v", err) |
| 372 | + } |
| 373 | + t.Logf("socks channel correctly rejected by ACL: %v", err) |
| 374 | +} |
| 375 | + |
| 376 | +// TestAuthSocksChannelAllowed verifies the ACL does not over-block: a user |
| 377 | +// explicitly granted socks (a regex matching the "socks" channel token) can |
| 378 | +// open a socks channel and reach a destination through the SOCKS5 proxy. |
| 379 | +func TestAuthSocksChannelAllowed(t *testing.T) { |
| 380 | + targetPort := availablePort() |
| 381 | + |
| 382 | + // a destination to CONNECT to through socks |
| 383 | + ln, err := net.Listen("tcp", "127.0.0.1:"+targetPort) |
| 384 | + if err != nil { |
| 385 | + t.Fatal(err) |
| 386 | + } |
| 387 | + defer ln.Close() |
| 388 | + go func() { |
| 389 | + for { |
| 390 | + conn, err := ln.Accept() |
| 391 | + if err != nil { |
| 392 | + return |
| 393 | + } |
| 394 | + conn.Write([]byte("VIA-SOCKS")) |
| 395 | + conn.Close() |
| 396 | + } |
| 397 | + }() |
| 398 | + |
| 399 | + s, err := chserver.NewServer(&chserver.Config{ |
| 400 | + KeySeed: "acl-socks-allowed", |
| 401 | + Socks5: true, |
| 402 | + }) |
| 403 | + if err != nil { |
| 404 | + t.Fatal(err) |
| 405 | + } |
| 406 | + s.Debug = debug |
| 407 | + // grant socks at the channel level (the channel token is the literal "socks") |
| 408 | + if err := s.AddUser("user", "pass", `^socks$`); err != nil { |
| 409 | + t.Fatal(err) |
| 410 | + } |
| 411 | + serverPort := availablePort() |
| 412 | + if err := s.Start("127.0.0.1", serverPort); err != nil { |
| 413 | + t.Fatal(err) |
| 414 | + } |
| 415 | + defer s.Close() |
| 416 | + |
| 417 | + sc, _, _ := dialChiselSSH(t, "127.0.0.1:"+serverPort, "user", "pass") |
| 418 | + defer sc.Close() |
| 419 | + // no remotes to declare; the socks channel is opened directly |
| 420 | + sendConfig(t, sc, []*settings.Remote{}) |
| 421 | + |
| 422 | + ch, reqs, err := sc.OpenChannel("chisel", []byte("socks")) |
| 423 | + if err != nil { |
| 424 | + t.Fatalf("socks channel rejected for a user granted socks: %v", err) |
| 425 | + } |
| 426 | + go ssh.DiscardRequests(reqs) |
| 427 | + defer ch.Close() |
| 428 | + |
| 429 | + // drive a minimal SOCKS5 CONNECT to the allowed local listener |
| 430 | + if _, err := ch.Write([]byte{0x05, 0x01, 0x00}); err != nil { // greeting: no-auth |
| 431 | + t.Fatal(err) |
| 432 | + } |
| 433 | + if _, err := io.ReadFull(ch, make([]byte, 2)); err != nil { // method selection |
| 434 | + t.Fatal(err) |
| 435 | + } |
| 436 | + port, err := strconv.Atoi(targetPort) |
| 437 | + if err != nil { |
| 438 | + t.Fatal(err) |
| 439 | + } |
| 440 | + connect := []byte{0x05, 0x01, 0x00, 0x01, 127, 0, 0, 1, byte(port >> 8), byte(port)} |
| 441 | + if _, err := ch.Write(connect); err != nil { |
| 442 | + t.Fatal(err) |
| 443 | + } |
| 444 | + reply := make([]byte, 10) |
| 445 | + if _, err := io.ReadFull(ch, reply); err != nil { |
| 446 | + t.Fatal(err) |
| 447 | + } |
| 448 | + if reply[1] != 0x00 { |
| 449 | + t.Fatalf("socks CONNECT failed, rep=%d", reply[1]) |
| 450 | + } |
| 451 | + buf := make([]byte, 16) |
| 452 | + n, err := ch.Read(buf) |
| 453 | + if err != nil && err != io.EOF { |
| 454 | + t.Fatalf("read via socks: %v", err) |
| 455 | + } |
| 456 | + if string(buf[:n]) != "VIA-SOCKS" { |
| 457 | + t.Fatalf("expected 'VIA-SOCKS' via socks, got %q", buf[:n]) |
| 458 | + } |
| 459 | + t.Logf("socks channel allowed and functional for a granted user") |
| 460 | +} |
0 commit comments